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
gitpane/config.py
ADDED
|
@@ -0,0 +1,1066 @@
|
|
|
1
|
+
"""Definitions that several GitPane modules must agree on, from shared values to the sizing rules of controls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import posixpath
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum, IntEnum
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from PySide6.QtCore import QMargins, QPoint, QRect
|
|
12
|
+
from PySide6.QtGui import QScreen
|
|
13
|
+
from PySide6.QtWidgets import QWidget
|
|
14
|
+
|
|
15
|
+
# Start of the names of the trace variables of Git and of the Git Credential Manager. Every place that removes traces
|
|
16
|
+
# from the environment of a program it starts reads this one value.
|
|
17
|
+
TRACE_ENVIRONMENT_PREFIXES = ("GIT_TRACE", "GCM_TRACE")
|
|
18
|
+
# Variables of GitPane itself and of Git or credential manager traces: a text editor GitPane starts to open a working
|
|
19
|
+
# copy file must not inherit them.
|
|
20
|
+
EDITOR_HIDDEN_ENVIRONMENT_PREFIXES = ("GITPANE_", *TRACE_ENVIRONMENT_PREFIXES)
|
|
21
|
+
ERROR_LIGHT_COLOR = "#b42318"
|
|
22
|
+
ERROR_DARK_COLOR = "#ff8a80"
|
|
23
|
+
# Green of a successful result, such as a Settings save, readable on a light or on a dark background.
|
|
24
|
+
SUCCESS_LIGHT_COLOR = "#1a7f37"
|
|
25
|
+
SUCCESS_DARK_COLOR = "#7ee2a8"
|
|
26
|
+
# Orange of a result that is not an error but asks to switch to another method (a server that refuses passwords, for
|
|
27
|
+
# example). Each keeps a contrast of at least 4.5:1 with its theme's background and stays clearly apart from the red.
|
|
28
|
+
WARNING_LIGHT_COLOR = "#b45309"
|
|
29
|
+
WARNING_DARK_COLOR = "#fdba74"
|
|
30
|
+
# Yellow of a recent folder whose check got no answer (Unknown): apart from the grey path, so it is noticed, and from the
|
|
31
|
+
# orange of Unavailable, so it is not read as gone. A dark gold on light backgrounds and a bright yellow on dark ones,
|
|
32
|
+
# each with a contrast of at least 4.5:1.
|
|
33
|
+
UNKNOWN_LIGHT_COLOR = "#8a6d00"
|
|
34
|
+
UNKNOWN_DARK_COLOR = "#fde047"
|
|
35
|
+
DARK_BACKGROUND_LIGHTNESS_LIMIT = 0.5
|
|
36
|
+
EDITOR_CONNECT_TIMEOUT_MS = 5000
|
|
37
|
+
EDITOR_LIVENESS_TIMEOUT_MS = 5000
|
|
38
|
+
EDITOR_HEARTBEAT_INTERVAL_MS = 250
|
|
39
|
+
# How long GitPane keeps a connection it answered open, for the bridge to read the answer and close it itself; past
|
|
40
|
+
# this delay, GitPane closes it.
|
|
41
|
+
EDITOR_ANSWERED_CLOSE_TIMEOUT_MS = 5000
|
|
42
|
+
EDITOR_IPC_LIMIT_BYTES = 65536
|
|
43
|
+
EDITOR_PENDING_LIMIT = 4
|
|
44
|
+
EDITOR_REQUEST_LIMIT = 1024
|
|
45
|
+
EDITOR_TOKEN_BYTES = 32
|
|
46
|
+
|
|
47
|
+
# The longest a size change asked just after a window is shown waits for the compositor to answer that show (the
|
|
48
|
+
# activation delivered to the window, and the configure that comes with it applied by Qt). Mutter answers in 8 to 15 ms,
|
|
49
|
+
# and Qt applies that configure at the end of the frame in progress; a window it never activates (opened while the user
|
|
50
|
+
# is in another application) still gets its size change once this bound is reached.
|
|
51
|
+
COMPOSITOR_ANSWER_TIMEOUT_MS = 1_000
|
|
52
|
+
# The longest one click waits, on the window's thread, for the Wayland compositor while GitPane asks for an activation
|
|
53
|
+
# token: binding the protocol the first time, the window that has the keyboard, then the token, all within this one
|
|
54
|
+
# bound. Mutter answers within a millisecond; a compositor that does not answer leaves the program to start without a
|
|
55
|
+
# token instead of freezing GitPane.
|
|
56
|
+
WAYLAND_ACTIVATION_TOKEN_TIMEOUT_MS = 500
|
|
57
|
+
# Longest activation token GitPane hands to a program; a longer one is refused. Mutter's tokens are about 45
|
|
58
|
+
# characters long (a UUID, "_TIME" and a time).
|
|
59
|
+
ACTIVATION_TOKEN_MAX_LENGTH = 256
|
|
60
|
+
# The longest each read of gnome-extensions may take when Settings checks whether GNOME can show desktop icons. The
|
|
61
|
+
# tool answers in a few milliseconds; the bound only matters for a loaded session or a Shell that does not answer, and
|
|
62
|
+
# a read with no answer in time gives no note rather than a wrong one.
|
|
63
|
+
SHELL_EXTENSION_READ_TIMEOUT_MS = 3_000
|
|
64
|
+
# The longest each of the three reads of the Secret Service may take when GitPane checks the default keyring collection.
|
|
65
|
+
# Each read answers in about 0.5 ms; a read with no answer in time counts as an unknown state, which gives no sentence.
|
|
66
|
+
KEYRING_READ_TIMEOUT_MS = 500
|
|
67
|
+
# The longest the read of the desktop's light or dark preference may take, through the Settings portal, when GitPane
|
|
68
|
+
# starts. The portal answers in under a millisecond; a read with no answer in time counts as no preference, which keeps
|
|
69
|
+
# the appearance Qt shows without it.
|
|
70
|
+
PORTAL_SETTING_READ_TIMEOUT_MS = 500
|
|
71
|
+
# The longest the gitpane command typed in a terminal waits for GitPane, started again in a session of its own, to
|
|
72
|
+
# show its first window before it gives the terminal back. A cold start on a slow disk takes a few seconds; past this
|
|
73
|
+
# delay the command gives the terminal back and says so, and GitPane goes on starting without that terminal.
|
|
74
|
+
DETACHED_READY_TIMEOUT_MS = 30_000
|
|
75
|
+
# How long the gitpane command still reads a GitPane that closed its ready channel without the ready signal, then waits
|
|
76
|
+
# for its end, counted from that close whatever is left of the wait above. A program GitPane started can keep its
|
|
77
|
+
# output open after GitPane ended, and a process ends a little after its channel closes: past this delay the command
|
|
78
|
+
# reports what it has and gives the terminal back.
|
|
79
|
+
DETACHED_STOP_GRACE_MS = 5_000
|
|
80
|
+
GIT_OPEN_TIMEOUT_MS = 5_000
|
|
81
|
+
# Characters refused in any name or revision placed on a Git command line: none can belong to a reference name or
|
|
82
|
+
# a revision, and each could split what Git or the logs receive.
|
|
83
|
+
COMMAND_LINE_CONTROL_CHARACTER_SET = frozenset({"\0", "\n", "\r"})
|
|
84
|
+
GIT_READ_TIMEOUT_MS = 30_000
|
|
85
|
+
# The longest a reference form waits, unseen, for its opening check. That check runs at most two batches of Git
|
|
86
|
+
# reads, each bounded by GIT_READ_TIMEOUT_MS, plus reads of reference files and its wait in the process queue: a slow
|
|
87
|
+
# Git is still explained by its own error, and this limit only ends a check that never answers (an error the read
|
|
88
|
+
# service does not catch, or a file read that blocks).
|
|
89
|
+
FORM_OPENING_CHECK_TIMEOUT_MS = 3 * GIT_READ_TIMEOUT_MS
|
|
90
|
+
# The whole history is read by one streamed process: a history of several hundred thousand commits can take a few
|
|
91
|
+
# minutes to read and show. A Git that stops writing is still stopped much sooner by the ordinary inactivity limit.
|
|
92
|
+
HISTORY_STREAM_TIMEOUT_MS = 600_000
|
|
93
|
+
# While the history is read, its new rows are shown at most this often: the first rows appear at once, and the window
|
|
94
|
+
# redraws a few times per second instead of once per chunk of Git output.
|
|
95
|
+
HISTORY_PUBLISH_INTERVAL_MS = 200
|
|
96
|
+
GIT_EXECUTABLE_PROBE_LIMIT_BYTES = 4096
|
|
97
|
+
GIT_MUTATION_TIMEOUT_MS = 60_000
|
|
98
|
+
TRASH_TIMEOUT_MS = 30_000
|
|
99
|
+
GIT_COMMIT_TIMEOUT_MS = 120_000
|
|
100
|
+
COMMIT_IDLE_TIMEOUT_MS = 120_000
|
|
101
|
+
# The longest wait, from the click, before the --log timing of a commit stops with an "abandoned" line: a window whose
|
|
102
|
+
# history never shows the commit must not be followed for the rest of the session. Well above the commit's own process
|
|
103
|
+
# limit (GIT_COMMIT_TIMEOUT_MS), the refresh of a large repository and an ordinary push.
|
|
104
|
+
COMMIT_TIMING_LIMIT_MS = 600_000
|
|
105
|
+
MUTATION_IDLE_TIMEOUT_MS = 60_000
|
|
106
|
+
# Placed in front of a native write command so it never descends into a submodule, even when the user set
|
|
107
|
+
# submodule.recurse: a write of the parent repository must leave every file of every submodule untouched.
|
|
108
|
+
SUBMODULE_RECURSION_OFF_ARGUMENTS = ("-c", "submodule.recurse=false")
|
|
109
|
+
# Placed in front of the Git commands that write or verify a rewritten history. Under Windows, and under macOS on a
|
|
110
|
+
# case-insensitive disk, git init writes core.ignorecase=true even in a bare repository, and fast-import then folds the
|
|
111
|
+
# case of the paths it deletes: removing "Secret" also removes "SECRET". History removal compares recorded paths byte
|
|
112
|
+
# for byte and never touches files on disk through these commands, so the case of a path must always count.
|
|
113
|
+
EXACT_PATH_CASE_ARGUMENTS = ("-c", "core.ignorecase=false")
|
|
114
|
+
# Where Git keeps every full reference name, and the namespaces under it of local branches, remote branches and tags.
|
|
115
|
+
# They build the names and patterns given to Git (for-each-ref, refspecs) and read back what it lists.
|
|
116
|
+
REFERENCE_ROOT_PREFIX = "refs/"
|
|
117
|
+
LOCAL_PREFIX = REFERENCE_ROOT_PREFIX + "heads/"
|
|
118
|
+
REMOTE_PREFIX = REFERENCE_ROOT_PREFIX + "remotes/"
|
|
119
|
+
TAG_PREFIX = REFERENCE_ROOT_PREFIX + "tags/"
|
|
120
|
+
# What rev-parse --symbolic-full-name answers for a detached HEAD; the grouped HEAD read of a commit ends with the same word.
|
|
121
|
+
DETACHED_HEAD_NAME = "HEAD"
|
|
122
|
+
# Number of characters of a commit id that the forms and the notices of the repository window show.
|
|
123
|
+
SHORT_OID_LENGTH = 12
|
|
124
|
+
# Separator of the folders in the paths Git writes and reads, whatever the system: Git always uses "/".
|
|
125
|
+
GIT_PATH_SEPARATOR = "/"
|
|
126
|
+
# Unicode category of control characters (C0, DEL and C1): never part of a path a user copied, and escaped when a path Git cited is shown.
|
|
127
|
+
CONTROL_CHARACTER_CATEGORY = "Cc"
|
|
128
|
+
# End of a line in what Git prints and in the text of the operation console.
|
|
129
|
+
LINE_SEPARATOR = "\n"
|
|
130
|
+
# Qt keeps the lines of a plain text document apart with this character in its raw text. The numbered editor turns it
|
|
131
|
+
# into a line break, so a file that holds one stays read-only.
|
|
132
|
+
QT_PARAGRAPH_SEPARATOR = "\u2029"
|
|
133
|
+
WORKTREE_REFRESH_INTERVAL_MS = 5_000
|
|
134
|
+
REBASE_REFRESH_INTERVAL_MS = 1_000
|
|
135
|
+
REBASE_CAPTURE_ATTEMPTS = 3
|
|
136
|
+
REBASE_FILE_LIMIT_BYTES = 256 * 1024
|
|
137
|
+
REBASE_TOTAL_LIMIT_BYTES = 1024 * 1024
|
|
138
|
+
REBASE_STEP_LIMIT = 5_000
|
|
139
|
+
REBASE_CAPTURE_TIMEOUT_MS = GIT_READ_TIMEOUT_MS
|
|
140
|
+
REBASE_CLOSE_REREAD_ATTEMPTS = 5
|
|
141
|
+
# The folder of the common Git directory where the reftable format keeps references and their logs; the files format
|
|
142
|
+
# has none, and keeps them as files under refs/ and logs/ instead.
|
|
143
|
+
GIT_REFTABLE_DIR_BASENAME = "reftable"
|
|
144
|
+
# The file Git keeps in a worktree's own Git directory while a merge waits for its commit: one commit ID per line, the
|
|
145
|
+
# commits that become the parents after HEAD.
|
|
146
|
+
MERGE_HEAD_BASENAME = "MERGE_HEAD"
|
|
147
|
+
# The file next to it that tells how the merge was started; Git writes exactly this text into it when the merge must
|
|
148
|
+
# keep every parent (git merge --no-ff), and otherwise drops from the commit any parent another parent already contains.
|
|
149
|
+
MERGE_MODE_BASENAME = "MERGE_MODE"
|
|
150
|
+
MERGE_MODE_NO_FAST_FORWARD = b"no-ff"
|
|
151
|
+
# Largest MERGE_HEAD or MERGE_MSG read when a merge is concluded; a real one holds a few lines.
|
|
152
|
+
MERGE_STATE_FILE_LIMIT_BYTES = 1024 * 1024
|
|
153
|
+
REPLAY_FILE_LIMIT_BYTES = 1024 * 1024
|
|
154
|
+
REPLAY_TOTAL_LIMIT_BYTES = 4 * REPLAY_FILE_LIMIT_BYTES
|
|
155
|
+
REPLAY_INSTRUCTION_LIMIT = 10000
|
|
156
|
+
REPLAY_CAPTURE_TIMEOUT_MS = 5000
|
|
157
|
+
REPLAY_REFRESH_INTERVAL_MS = 2000
|
|
158
|
+
# Bounds on reading the Cherry-pick and Revert journals earlier versions left: the size of one record and how many are read.
|
|
159
|
+
REPLAY_RECORD_LIMIT_BYTES = 4 * 1024 * 1024
|
|
160
|
+
REPLAY_RECORD_COUNT_LIMIT = 1000
|
|
161
|
+
# How many files a refused Cherry-pick or Revert names when it would remove a submodule folder that still holds files
|
|
162
|
+
# its own history does not record.
|
|
163
|
+
SUBMODULE_LOSS_PATH_DISPLAY_LIMIT = 10
|
|
164
|
+
WORKTREE_OUTPUT_LIMIT_BYTES = 32 * 1024 * 1024
|
|
165
|
+
WORKTREE_INDEX_LIMIT_BYTES = 128 * 1024 * 1024
|
|
166
|
+
WORKTREE_FILE_WORKER_COUNT = 2
|
|
167
|
+
MUTATION_RECORD_LIMIT_BYTES = 64 * 1024
|
|
168
|
+
IGNORE_RULE_LIMIT_BYTES = 8 * 1024 * 1024
|
|
169
|
+
GIT_MUTATION_ENVIRONMENT_NAME_SET = frozenset({"GIT_INDEX_FILE"})
|
|
170
|
+
GIT_ALTERNATE_OBJECT_ENVIRONMENT_NAME = "GIT_ALTERNATE_OBJECT_DIRECTORIES"
|
|
171
|
+
GIT_INERT_EDITOR = ":"
|
|
172
|
+
STAGING_WIDTH, STAGING_HEIGHT = 1200, 800
|
|
173
|
+
STAGING_MIN_WIDTH, STAGING_MIN_HEIGHT = 800, 560
|
|
174
|
+
FILE_HISTORY_ACTIVE_COMPARISON_LIMIT = 1
|
|
175
|
+
FILE_HISTORY_BATCH_SIZE = 256
|
|
176
|
+
FILE_HISTORY_CACHE_ENTRY_LIMIT = 1024
|
|
177
|
+
FILE_HISTORY_COMPARISON_LIMIT = 10_000
|
|
178
|
+
FILE_HISTORY_TURN_BUDGET_MS = 5
|
|
179
|
+
# A Git-launched shell may describe its own repository; each read must discover the requested directory instead.
|
|
180
|
+
GIT_LOCAL_ENVIRONMENT_NAMES = (
|
|
181
|
+
"GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
182
|
+
"GIT_NAMESPACE", "GIT_PREFIX", "GIT_IMPLICIT_WORK_TREE", "GIT_GRAFT_FILE", "GIT_SHALLOW_FILE", "GIT_REPLACE_REF_BASE",
|
|
183
|
+
"GIT_CONFIG", "GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT", "GIT_CEILING_DIRECTORIES", "GIT_DISCOVERY_ACROSS_FILESYSTEM",
|
|
184
|
+
)
|
|
185
|
+
GIT_LOCAL_ENVIRONMENT_PREFIXES = ("GIT_CONFIG_KEY_", "GIT_CONFIG_VALUE_")
|
|
186
|
+
# Command-scope setting given to every Git command GitPane starts, as a (key, value) pair. Under macOS and Windows, a
|
|
187
|
+
# repository configured for fsmonitor makes Git start a background daemon that stays in the process group of the command
|
|
188
|
+
# that started it, and a supervised command only ends once its whole group is gone. Passed through GIT_CONFIG_COUNT in the
|
|
189
|
+
# environment rather than with -c, so no command line changes.
|
|
190
|
+
GIT_FSMONITOR_SETTING = ("core.fsmonitor", "false")
|
|
191
|
+
# Turns the millisecond time limits of this file into the seconds that Python's own waits take.
|
|
192
|
+
MILLISECONDS_PER_SECOND = 1000
|
|
193
|
+
# Turns a reading of the monotonic clock, in nanoseconds, into the milliseconds that timings are written in.
|
|
194
|
+
NANOSECONDS_PER_MILLISECOND = 1_000_000
|
|
195
|
+
# A shell reports a process ended by signal n with this base plus n, and so do GitPane's own exit codes.
|
|
196
|
+
SIGNAL_EXIT_CODE_BASE = 128
|
|
197
|
+
# Interpreter option that keeps the current folder (python -m) and the folder of a script started by its path (python
|
|
198
|
+
# script.py) out of sys.path, so a gitpane folder in the folder a program starts from is never imported instead of the
|
|
199
|
+
# installed GitPane.
|
|
200
|
+
SAFE_PATH_OPTION = "-P"
|
|
201
|
+
# Interpreter option that runs a module given by its dotted name.
|
|
202
|
+
MODULE_OPTION = "-m"
|
|
203
|
+
# Interpreter option that runs the Python text given as the next argument.
|
|
204
|
+
CODE_OPTION = "-c"
|
|
205
|
+
# Interpreter option of isolated mode: the user's Python environment variables, user site directory and current folder stay out of the child.
|
|
206
|
+
ISOLATED_MODE_OPTION = "-I"
|
|
207
|
+
# Interpreter option that loads no site directory at all, so only the standard library can be imported.
|
|
208
|
+
NO_SITE_OPTION = "-S"
|
|
209
|
+
# Interpreter options of a standard-library script GitPane starts: isolated mode, without any site directory.
|
|
210
|
+
ISOLATED_INTERPRETER_OPTIONS = (ISOLATED_MODE_OPTION, NO_SITE_OPTION)
|
|
211
|
+
# Interpreter option that ignores the PYTHON* variables the interpreter reads itself (PYTHONPATH, PYTHONHOME,
|
|
212
|
+
# PYTHONSTARTUP, PYTHONNOUSERSITE...). The site module still reads PYTHONUSERBASE, which moves the user's site-packages
|
|
213
|
+
# folder.
|
|
214
|
+
IGNORE_ENVIRONMENT_OPTION = "-E"
|
|
215
|
+
# Interpreter option that leaves the user's site-packages folder off the import path.
|
|
216
|
+
NO_USER_SITE_OPTION = "-s"
|
|
217
|
+
# Options of a GitPane module started as a program of its own (account worker, rebase editor bridge, SSH passphrase
|
|
218
|
+
# prompt) by a GitPane that has the user's site-packages folder on its path. Unlike isolated mode, that folder stays on
|
|
219
|
+
# the path: pip installs GitPane there when the Python's own folder is read-only.
|
|
220
|
+
APPLICATION_MODULE_INTERPRETER_OPTIONS = (IGNORE_ENVIRONMENT_OPTION, SAFE_PATH_OPTION)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def application_module_interpreter_options(no_user_site: bool) -> tuple[str, ...]:
|
|
224
|
+
"""Answer the options of a GitPane module started as a program of its own, with the user's site-packages folder only when the GitPane that starts it has it.
|
|
225
|
+
|
|
226
|
+
``no_user_site`` tells whether that GitPane runs without the folder: started with -s or -I, or with
|
|
227
|
+
PYTHONNOUSERSITE set (``sys.flags.no_user_site``). Under -E the program ignores PYTHONNOUSERSITE, so the folder is
|
|
228
|
+
left out by its own option; otherwise a gitpane installed there with pip --user could be imported by the program
|
|
229
|
+
while the GitPane that starts it runs another version, and both sides would no longer speak the same protocol.
|
|
230
|
+
"""
|
|
231
|
+
if no_user_site:
|
|
232
|
+
return (*APPLICATION_MODULE_INTERPRETER_OPTIONS, NO_USER_SITE_OPTION)
|
|
233
|
+
return APPLICATION_MODULE_INTERPRETER_OPTIONS
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
PROCESS_IDLE_TIMEOUT_MS = 10_000
|
|
237
|
+
GIT_TRANSPORT_TIMEOUT_MS = 300_000
|
|
238
|
+
GIT_CLONE_TIMEOUT_MS = 900_000
|
|
239
|
+
AUTHENTICATION_TIMEOUT_MS = 600_000
|
|
240
|
+
# Longest wait for the `git ls-remote` that tests a username + password account on its server before it is saved.
|
|
241
|
+
ACCOUNT_CHECK_TIMEOUT_MS = 60_000
|
|
242
|
+
# Longest wait for the `git ls-remote` that reads a remote repository's branches while a URL is typed in the clone form.
|
|
243
|
+
GIT_BRANCH_LIST_TIMEOUT_MS = 60_000
|
|
244
|
+
# How many times a Git network step is started again after Git Credential Manager could not read back a secret it keeps,
|
|
245
|
+
# a failure that passes by itself: at most this many retries after the first attempt, so one more start in all.
|
|
246
|
+
CREDENTIAL_READ_RETRY_LIMIT = 3
|
|
247
|
+
# Wait between two starts of such a step, which gives the credential store time to answer again.
|
|
248
|
+
CREDENTIAL_READ_RETRY_DELAY_MS = MILLISECONDS_PER_SECOND
|
|
249
|
+
# How long a browser sign-in waits for the browser to answer, as chosen by the user: past it, the exchange with Git
|
|
250
|
+
# Credential Manager is stopped and nothing is saved.
|
|
251
|
+
BROWSER_SIGN_IN_TIMEOUT_MS = 120_000
|
|
252
|
+
# Extra time the process supervisor gives a browser sign-in beyond that wait. The worker stops the browser wait itself;
|
|
253
|
+
# this margin covers the identity check and the secure store that follow a late answer, and stops a worker that no
|
|
254
|
+
# longer answers at all.
|
|
255
|
+
BROWSER_SIGN_IN_STOP_GRACE_MS = 90_000
|
|
256
|
+
# Longest time one check of the system credential store may take, for every account it covers together: it only asks
|
|
257
|
+
# the credential helper whether each account's secret is still stored, never interactively and without any network
|
|
258
|
+
# request, so a few seconds per account are plenty. Past it, the accounts not answered yet stay unknown.
|
|
259
|
+
SECRET_LOOKUP_TIMEOUT_MS = 10_000
|
|
260
|
+
# Extra time the process supervisor gives that check beyond its own limit, to start its worker and stop a late helper.
|
|
261
|
+
SECRET_LOOKUP_STOP_GRACE_MS = 10_000
|
|
262
|
+
# Longest time the erase of a secret an exchange left behind may take: it runs without any prompt, and nobody waits for
|
|
263
|
+
# it. What it does not erase in time is erased by the next test of the same account or by the account's removal.
|
|
264
|
+
SECRET_ERASE_TIMEOUT_MS = 10_000
|
|
265
|
+
# Longest wait for the one request that recognises a server (GitHub or GitLab) after it refused a password; the same
|
|
266
|
+
# default as the server detection of Git Credential Manager.
|
|
267
|
+
SERVER_PROBE_TIMEOUT_MS = 2_000
|
|
268
|
+
# Longest wait for the system to stop a process and every process it started, once its own time limit is over.
|
|
269
|
+
PROCESS_TREE_STOP_TIMEOUT_MS = 5_000
|
|
270
|
+
AUTHENTICATION_PREFERENCE_KEY = "authentication_selection"
|
|
271
|
+
# How each connected account signs in, by account namespace. Kept apart from the account list so an older GitPane still
|
|
272
|
+
# open at the same time, which does not know this key, keeps reading the account list.
|
|
273
|
+
ACCOUNT_SIGN_IN_METHOD_PREFERENCE_KEY = "account_sign_in_method"
|
|
274
|
+
# The answer kept for the question that links a pushed branch to its remote branch, by shared Git folder of the
|
|
275
|
+
# repository. Kept apart from the account data, whose JSON refuses any key it does not know.
|
|
276
|
+
PUSH_TRACKING_PREFERENCE_KEY = "push_tracking_by_repository"
|
|
277
|
+
AUTHENTICATION_CONFIG_FILE_LIMIT = 128
|
|
278
|
+
AUTHENTICATION_CONFIG_LIMIT_BYTES = 8 * 1024 * 1024
|
|
279
|
+
GIT_DEFAULT_TEMPLATE_DIR = "%(prefix)/share/git-core/templates"
|
|
280
|
+
SSH_PROMPT_TIMEOUT_MS = 120_000
|
|
281
|
+
SSH_IPC_CONNECT_TIMEOUT_MS = 5_000
|
|
282
|
+
SSH_PROMPT_LIMIT_BYTES = 16 * 1024
|
|
283
|
+
SSH_RESPONSE_LIMIT_BYTES = 16 * 1024
|
|
284
|
+
SSH_IPC_LIMIT_BYTES = 64 * 1024
|
|
285
|
+
SSH_PENDING_CONNECTION_LIMIT = 4
|
|
286
|
+
SSH_PROMPT_COUNT_LIMIT = 16
|
|
287
|
+
SSH_NONCE_BYTES = 32
|
|
288
|
+
SSH_ENVIRONMENT_NAMES = ("GITPANE_SSH_PIPE", "GITPANE_SSH_NONCE", "GITPANE_SSH_OPERATION", "GITPANE_SSH_OWNER_PID")
|
|
289
|
+
TRANSPORT_IDLE_TIMEOUT_MS = 120_000
|
|
290
|
+
AUTHENTICATION_IDLE_TIMEOUT_MS = 600_000
|
|
291
|
+
TOOL_DOWNLOAD_TIMEOUT_SECONDS = 120
|
|
292
|
+
# Longest wait for the mount command of macOS, which asks every mount point and so may wait on a dead network share;
|
|
293
|
+
# past it, the recent folders are checked in a single group, as when no mount table can be read.
|
|
294
|
+
MOUNT_TABLE_TIMEOUT_SECONDS = 2
|
|
295
|
+
TOOL_BUILD_TIMEOUT_SECONDS = 600
|
|
296
|
+
# Longest wait of a test or of a qualification tool for one step of its scenario, such as a window state, an
|
|
297
|
+
# operation's result or one Git command it runs; the application itself never waits on it.
|
|
298
|
+
SCENARIO_STEP_TIMEOUT_MS = 30_000
|
|
299
|
+
QUALIFICATION_SCENARIO_TIMEOUT_SECONDS = 60
|
|
300
|
+
# Bounds the single pytest child process the measuring tool starts for a reading. Not a test timeout. One hour was
|
|
301
|
+
# sized for the process ceiling tests a reading used to run; since they were removed, a reading only collects the suite.
|
|
302
|
+
PROCESS_MEASURE_TIMEOUT_SECONDS = 3_600
|
|
303
|
+
TRACE_PAGE_SIZE = 200
|
|
304
|
+
TRACE_UNIT_LIMIT_BYTES = 16 * 1024 * 1024
|
|
305
|
+
TRACE_STORAGE_LIMIT_BYTES = 128 * 1024 * 1024
|
|
306
|
+
TRACE_VIEW_LIMIT_BYTES = 64 * 1024
|
|
307
|
+
TRACE_RENDER_INTERVAL_MS = 30
|
|
308
|
+
# The byte counter under the console is a word-wrapped label: every rewrite asks the whole window to lay itself out
|
|
309
|
+
# again, and twice a second is all the eye can follow while output streams in.
|
|
310
|
+
TRACE_NAVIGATION_INTERVAL_MS = 500
|
|
311
|
+
OPERATION_WIDTH, OPERATION_HEIGHT = 900, 560
|
|
312
|
+
OPERATION_KEEP_OPEN_PREFERENCE_KEY = "operation_keep_open"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ConsoleOperation(Enum):
|
|
316
|
+
"""Kind of work shown by the shared operation console; the kind alone decides whether the console offers Abort."""
|
|
317
|
+
|
|
318
|
+
FETCH = "fetch"
|
|
319
|
+
"""Fetch and pull."""
|
|
320
|
+
PUSH = "push"
|
|
321
|
+
"""Push, commit and push, and the tag push offered after creating a tag."""
|
|
322
|
+
REMOTE_DELETE = "remote-delete"
|
|
323
|
+
"""Deletion of a remote branch or tag."""
|
|
324
|
+
CLONE = "clone"
|
|
325
|
+
"""Clone, including the retry of its submodules."""
|
|
326
|
+
SUBMODULE_INIT = "submodule-init"
|
|
327
|
+
STASH_POP = "stash-pop"
|
|
328
|
+
REBASE = "rebase"
|
|
329
|
+
"""Rebase start, todo edit, Continue and Abort."""
|
|
330
|
+
REFERENCE = "reference"
|
|
331
|
+
"""Checkout, branch, tag, reset, cherry-pick and revert, including their Continue and Abort."""
|
|
332
|
+
HISTORY_REWRITE = "history-rewrite"
|
|
333
|
+
"""Confirmed history publication whose stop waits for safe reconciliation."""
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# The end of every refusal of an outdated reference capture. The form that shows it blocks the repository window, so
|
|
337
|
+
# the user closes it first; {refresh} and {shortcut} are filled from the real action and its shortcut on this system.
|
|
338
|
+
STALE_CAPTURE_ADVICE = " Close this window, click {refresh} ({shortcut}), then try again."
|
|
339
|
+
# The end of a refusal after which GitPane reads the repository again by itself, as a refused checkout does: clicking
|
|
340
|
+
# Refresh would add nothing, the user only closes the form and tries again.
|
|
341
|
+
REREAD_CAPTURE_ADVICE = " GitPane has read the repository again: close this window, then try again."
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class SharedText(Enum):
|
|
345
|
+
"""Texts shown by several windows, or written by code outside the interface, one member per text so a misspelt name is an error."""
|
|
346
|
+
|
|
347
|
+
SETTING_BUTTON = "Settings…"
|
|
348
|
+
"""The settings button of the welcome window."""
|
|
349
|
+
SETTING_BUTTON_MISSING_GIT = "Open settings"
|
|
350
|
+
"""The same button while no usable Git has been found: it then only opens the settings to choose one."""
|
|
351
|
+
SETTING = "Settings"
|
|
352
|
+
"""The settings action of the repository window and the title of the settings window."""
|
|
353
|
+
REFRESH = "Refresh"
|
|
354
|
+
"""The action of the repository window that reads the repository again, which the refusals of an outdated reference capture name."""
|
|
355
|
+
RECENT_REPOSITORY = "Recent repositories"
|
|
356
|
+
HISTORY_REFRESH = "Refreshing…"
|
|
357
|
+
"""The status bar text while the history is read again."""
|
|
358
|
+
TEXT_SEARCH_HELP = "Case-insensitive search; * matches characters within a word, including hyphens, but not whitespace. Other characters are literal."
|
|
359
|
+
"""The tooltip of the find field of the commit message and of the diff."""
|
|
360
|
+
DETAIL_UNAVAILABLE = "Details unavailable"
|
|
361
|
+
"""The start of the error a commit read gives when Git did not answer with its details."""
|
|
362
|
+
CHILD_LOADING = "Finding children…"
|
|
363
|
+
TERMINAL_CHOOSE = "Choose terminal"
|
|
364
|
+
RESULT_DETAILS = "Show details"
|
|
365
|
+
"""The button next to a result in Prepare changes; the commit warning that tells the user to click it quotes it."""
|
|
366
|
+
COPY_DETAILS = "Copy details"
|
|
367
|
+
"""The button that copies the shown details to the clipboard: the error window and the sign-in form read it here, while the operation console, the merge recovery window and the history rewrite windows still write the same text themselves."""
|
|
368
|
+
FAILED = "Failed"
|
|
369
|
+
"""The status line of a console that failed, and a failed or refused step of Delete branch; the console lines written outside the interface use the same word."""
|
|
370
|
+
NOT_STARTED = "Not started"
|
|
371
|
+
"""A step of Delete branch that never began; the console lines written outside the interface use the same word."""
|
|
372
|
+
OUTCOME_UNKNOWN = "Outcome unknown"
|
|
373
|
+
"""An end GitPane could not observe: the status line of a console, and a step of Delete branch or Delete tag."""
|
|
374
|
+
REBASE_EDITOR_CLOSE_WAIT = "Waiting for the rebase editor: continue or abort the edit in GitPane."
|
|
375
|
+
"""The start of the banner of a repository window that waits, before it closes or changes repository, for an edit Git asked for."""
|
|
376
|
+
REBASE_APPLYING_COMMITS = "Applying commits…"
|
|
377
|
+
"""The progress line of a rebase while Git applies the commits: after the start, after an edit answered or lost, and after Continue rebase."""
|
|
378
|
+
SUBMODULE_CHECKOUT_REFUSED_START = "Submodule {submodule} could not be checked out: "
|
|
379
|
+
"""The start of the sentence that explains a submodule whose checkout Git refused, in the console, the diagnostic and the clone form."""
|
|
380
|
+
CHECKOUT_REFUSED_WINDOWS_NAME = "Windows does not allow the file name '{path}'."
|
|
381
|
+
"""Why Git refused a path under Windows; it also comes before the link reason when Windows refuses a part of the path. A path with ? is followed by CHECKOUT_REFUSED_QUESTION_MARK."""
|
|
382
|
+
CHECKOUT_REFUSED_OTHER_NAME = "Git did not accept the file name '{path}' on this system."
|
|
383
|
+
"""Why Git refused a path under Linux or macOS, where the rule may be Git's own rather than the file system's."""
|
|
384
|
+
CHECKOUT_REFUSED_PROTECTED_PATH = "Git protects the path '{path}': it never writes a .git folder, or a name Windows reads as one, inside a repository."
|
|
385
|
+
"""Why Git refused a path with a .git component, or one Windows reads as .git (git~1, .git with dots or spaces at the end), on every system: Git protects it, not the file system."""
|
|
386
|
+
CHECKOUT_REFUSED_PROTECTED_LINK = "Git protects the file name '{path}': it never writes it as a symbolic link."
|
|
387
|
+
"""Why Git refused a path whose last part is, without case, exactly .gitmodules, .gitattributes, .gitignore or .mailmap, on every system: Git refuses these names only as a symbolic link."""
|
|
388
|
+
CHECKOUT_REFUSED_QUESTION_MARK = "Git shows a control character in a file name as ?."
|
|
389
|
+
"""Added after the reason of a cited path that holds ?: Git writes ? for any control character, so the text never names one."""
|
|
390
|
+
SUBMODULE_CHECKOUT_REFUSED_END = "Other files of this submodule may have the same problem; Retry submodules cannot write them while these names exist in the repository. The rest of the clone is kept and can be opened."
|
|
391
|
+
"""The end of that sentence: Git cites only the first name it refuses, and Retry submodules stays offered."""
|
|
392
|
+
CLONED_SUBMODULE_CHANGED = "The folder of submodule {submodule} changed after Git cloned it, so GitPane did not check it out. Existing files were preserved."
|
|
393
|
+
"""Why GitPane stopped before checking out a submodule Git had cloned without its files: something appeared in its folder meanwhile."""
|
|
394
|
+
CLONED_SUBMODULE_CHECKOUT_NOT_STARTED = ("GitPane did not check out the submodules Git cloned without their files: the clone no longer holds its repository."
|
|
395
|
+
" Click Retry submodules to check them out.")
|
|
396
|
+
"""Why the clone's step that checks out the submodules Git cloned without files did not run: the clone had already released its repository."""
|
|
397
|
+
CLONED_SUBMODULE_CHECKOUT_STEP = "Check out cloned submodules"
|
|
398
|
+
"""The step of a clone that checks out the submodules Git cloned but left without files after refusing another submodule's checkout."""
|
|
399
|
+
UPSTREAM_LINK_STEP = "Set upstream branch"
|
|
400
|
+
"""The step of a Push that links the pushed branch to its remote branch, shown in the console and in the details of the Push window."""
|
|
401
|
+
UPSTREAM_LINKED = "{branch} now tracks {tracking}."
|
|
402
|
+
"""The console line once the pushed branch follows the remote-tracking branch it was pushed to."""
|
|
403
|
+
UPSTREAM_NOT_LINKED = "{branch} was pushed, but it was not linked to {tracking}: {cause}"
|
|
404
|
+
"""The red console line when the link asked for after a successful Push was not set; the Push itself stays a success."""
|
|
405
|
+
UPSTREAM_NO_TRACKING_BRANCH = "no remote-tracking branch of {remote} receives {destination}"
|
|
406
|
+
"""The cause of that line when no Fetch refspec of the remote maps the pushed destination to a remote-tracking branch."""
|
|
407
|
+
UPSTREAM_KEPT = "{branch} already has an upstream branch; it was left unchanged."
|
|
408
|
+
"""The neutral console line when the pushed branch already has an upstream once the repository is read again after the Push: GitPane leaves that upstream as it is."""
|
|
409
|
+
UPSTREAM_OTHER_UPSTREAM = "Git reports {upstream} as its upstream branch"
|
|
410
|
+
"""The cause of the not-linked line when Git accepted the link but resolves another upstream, as with several branch.<name>.remote values."""
|
|
411
|
+
UPSTREAM_NO_UPSTREAM = "Git reports no upstream branch"
|
|
412
|
+
"""The cause of the not-linked line when Git accepted the link but the repository, read again, shows the branch without any upstream."""
|
|
413
|
+
UPSTREAM_UNREAD = "the upstream branch could not be read back: {cause}"
|
|
414
|
+
"""The cause of the not-linked line when the repository could not be read again after the link, so the link is not known to be set."""
|
|
415
|
+
PUSH_TRACKING_UNREADABLE = "the " + PUSH_TRACKING_PREFERENCE_KEY + " entry of the preferences file is not a JSON object: remove it to go back to Ask for every repository"
|
|
416
|
+
"""Why a kept tracking choice cannot be saved: the stored value of every repository's choices is not an object, so only removing it from gitpane.ini lets GitPane save again."""
|
|
417
|
+
SAVE_TRACKING_CHOICE = "Save tracking choice"
|
|
418
|
+
"""The operation named in the diagnostic and the log when the answer kept by Don't ask again cannot be written to the preferences file."""
|
|
419
|
+
PUSH_TRACKING_NOT_SAVED = "Don't ask again was not saved: {cause}"
|
|
420
|
+
"""The status bar line of the repository window when the tracking answer to keep cannot be written to the preferences file; the Push still goes with that answer."""
|
|
421
|
+
COMMIT_PUSH_CANCELLED = "Commit created. Push cancelled: the commit stays local."
|
|
422
|
+
"""The status bar line of the repository window when the user answers Cancel to the tracking question of a Commit & push: the commit exists, nothing was sent."""
|
|
423
|
+
COMMIT_PUSH_NOT_SENT = "Commit created. Push not sent: this window may no longer push this repository. The commit stays local."
|
|
424
|
+
"""The status bar line of the repository window when a Commit & push's created commit cannot be pushed because the window lost its authority to manage the repository: nothing was sent."""
|
|
425
|
+
CREDENTIAL_READ_RETRY = "The credential manager could not read the stored secret; trying again in {seconds} s ({attempt} of {limit})."
|
|
426
|
+
"""The console and --log line of a Git network step started again because the credential manager could not read back a secret it keeps."""
|
|
427
|
+
CREDENTIAL_READ_RETRY_CANCELLED = "Cancelled while waiting to try again: Git was not started again."
|
|
428
|
+
"""The cause of an operation cancelled, or of GitPane closing, while a network step waited to be started again."""
|
|
429
|
+
REFERENCE_CHANGED_BEFORE_OPENING = "The repository changed since GitPane last read it (branches, tags or Git settings)." + STALE_CAPTURE_ADVICE
|
|
430
|
+
"""A reference form refused when it opens: the references or the Git settings differ from the capture its menu was built on."""
|
|
431
|
+
SELECTED_REFERENCE_OR_REPOSITORY_CHANGED = ("The repository changed since GitPane last read it (the selected branch or tag, other branches or tags, or Git settings)."
|
|
432
|
+
+ STALE_CAPTURE_ADVICE)
|
|
433
|
+
"""Delete branch or Delete tag refused when it opens: the clicked branch or tag, or the rest of the capture its menu was built on, changed."""
|
|
434
|
+
SELECTED_LOCAL_BRANCH_CHANGED = "The selected local branch changed or was deleted since GitPane last read it." + STALE_CAPTURE_ADVICE
|
|
435
|
+
"""A branch form refused because the local branch it acts on moved, disappeared or is no longer a plain local branch."""
|
|
436
|
+
HEAD_CHANGED_BEFORE_OPENING = "The current branch or commit changed since GitPane last read the repository." + STALE_CAPTURE_ADVICE
|
|
437
|
+
"""Checkout branch or Checkout revision refused when it opens: HEAD differs from the capture its menu was built on."""
|
|
438
|
+
SELECTED_REFERENCE_CHANGED = "The selected branch or tag changed or was deleted since GitPane last read it." + STALE_CAPTURE_ADVICE
|
|
439
|
+
"""A checkout or a deletion refused because the branch or tag it acts on no longer points where the capture said, or is gone."""
|
|
440
|
+
FORM_CHECK_UNANSWERED = "GitPane got no answer while checking the repository for this form; try again."
|
|
441
|
+
"""A reference form closed before it was ever shown because its opening check gave no answer within its limit; the next click opens a new form."""
|
|
442
|
+
NOT_A_LOCAL_BRANCH = "GitPane does not know '{branch}' as a local branch: the branches may have changed since it last read the repository." + STALE_CAPTURE_ADVICE
|
|
443
|
+
"""A deletion of local branches refused before any process: a requested name is not a plain local branch of the capture."""
|
|
444
|
+
REFERENCE_CHANGED_DURING_READING = ("The branches, tags or current commit changed while GitPane was reading them."
|
|
445
|
+
" Close any open form, click {refresh} ({shortcut}), then try again.")
|
|
446
|
+
"""A reference read that saw the files change while it ran, shown in the status bar of the repository window or in a form."""
|
|
447
|
+
SELECTED_REFERENCE_CHANGED_OR_SYMBOLIC = ("The selected branch or tag changed since GitPane last read it, or it is now another name for a branch (a symbolic reference)."
|
|
448
|
+
+ REREAD_CAPTURE_ADVICE)
|
|
449
|
+
"""A checkout refused when it starts: the chosen branch or tag moved, disappeared or became a symbolic reference."""
|
|
450
|
+
CHECKOUT_REFERENCES_OR_HEAD_CHANGED = "The branches, tags or current commit changed since GitPane last read the repository." + REREAD_CAPTURE_ADVICE
|
|
451
|
+
"""A checkout refused when it starts: the references or the current commit are no longer those the form was opened on."""
|
|
452
|
+
CHECKOUT_LOCAL_BRANCH_GONE = "The local branch to reset no longer exists." + REREAD_CAPTURE_ADVICE
|
|
453
|
+
"""A checkout that resets a local branch onto its remote branch, refused when it starts because that local branch was deleted."""
|
|
454
|
+
CHECKOUT_CHANGED_BEFORE_LAUNCH = ("The repository changed just before Git started the checkout (branches, tags, current commit or Git settings)."
|
|
455
|
+
+ REREAD_CAPTURE_ADVICE)
|
|
456
|
+
"""A checkout refused at the last moment: the references or Git settings changed, or the request stopped being current, while it waited to start."""
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# Only a network transfer can be stopped midway from the console: it waits on a server and stopping it leaves the
|
|
460
|
+
# repository as it was. Every other console runs a local write that must finish, so it hides the Abort button, and
|
|
461
|
+
# Escape or the close button do not stop it either. A paused rebase, cherry-pick or revert is abandoned from the
|
|
462
|
+
# repository window instead.
|
|
463
|
+
CONSOLE_ABORTABLE_OPERATION_SET = frozenset((ConsoleOperation.FETCH, ConsoleOperation.PUSH, ConsoleOperation.REMOTE_DELETE, ConsoleOperation.CLONE, ConsoleOperation.SUBMODULE_INIT, ConsoleOperation.HISTORY_REWRITE))
|
|
464
|
+
DIAGNOSTIC_DECODE_LIMIT = 8
|
|
465
|
+
# The OAuth authorization code field. The same word also names exit, HTTP and Git error codes, so the secret filter lets
|
|
466
|
+
# a short number after it through; every other value stays hidden.
|
|
467
|
+
AUTHORIZATION_CODE_FIELD_NAME = "code"
|
|
468
|
+
SENSITIVE_FIELD_NAMES = (
|
|
469
|
+
"authorization", "proxy-authorization", "cookie", "set-cookie", "password", "passwd", "access_token", "refresh_token", "id_token", "token", AUTHORIZATION_CODE_FIELD_NAME,
|
|
470
|
+
"code_verifier", "verifier", "state", "nonce", "client_secret", "secret", "api_key", "apikey", "credential",
|
|
471
|
+
)
|
|
472
|
+
# A value shorter than this is never hidden in diagnostics and logs: hiding a one-character value, such as the "0" of
|
|
473
|
+
# a trace switch, rewrites every line it appears in (times, object names, counts) and protects nothing.
|
|
474
|
+
SENSITIVE_VALUE_MIN_LENGTH = 2
|
|
475
|
+
# The environment variable through which Git Credential Manager reads each of its settings that GitPane depends on, named
|
|
476
|
+
# as in Git configuration. Git Credential Manager reads such a variable before any Git configuration, even before a
|
|
477
|
+
# setting the user scoped to one address, and it only matches a URL-scoped setting against the host Git sends it, never
|
|
478
|
+
# against a repository path; a helper started with these variables therefore finds its account whatever URL Git chose
|
|
479
|
+
# that helper for, whatever the user's own configuration says. Every other setting of Git Credential Manager (its proxy,
|
|
480
|
+
# its prompts window…) stays the user's.
|
|
481
|
+
GCM_ENVIRONMENT_NAME_BY_SETTING = {"provider": "GCM_PROVIDER", "namespace": "GCM_NAMESPACE", "credentialStore": "GCM_CREDENTIAL_STORE",
|
|
482
|
+
"interactive": "GCM_INTERACTIVE", "traceSecrets": "GCM_TRACE_SECRETS", "allowWindowsAuth": "GCM_ALLOW_WINDOWSAUTH",
|
|
483
|
+
"gitHubAuthModes": "GCM_GITHUB_AUTHMODES", "gitLabAuthModes": "GCM_GITLAB_AUTHMODES", "bitbucketAuthModes": "GCM_BITBUCKET_AUTHMODES"}
|
|
484
|
+
GCM_TRACE_SECRETS_NAME = GCM_ENVIRONMENT_NAME_BY_SETTING["traceSecrets"]
|
|
485
|
+
# Environment names that look secret but hold no secret: the Git Credential Manager switch that keeps secrets out of
|
|
486
|
+
# its own trace has "SECRET" in its name, yet its value is only "0" or "1".
|
|
487
|
+
NON_SECRET_ENVIRONMENT_NAME_SET = frozenset({GCM_TRACE_SECRETS_NAME})
|
|
488
|
+
NETWORK_ENVIRONMENT_VALUE_SET_BY_NAME = {
|
|
489
|
+
"GIT_TRACE": frozenset({"1"}), "GIT_TRACE_CURL": frozenset({"1"}), "GIT_TRACE_CURL_NO_DATA": frozenset({"0", "1"}),
|
|
490
|
+
"GIT_TRACE_REDACT": frozenset({"1"}), "GCM_TRACE": frozenset({"1"}), GCM_TRACE_SECRETS_NAME: frozenset({"0"}),
|
|
491
|
+
"GIT_CONFIG_NOSYSTEM": frozenset({"1"}), "NO_PROXY": frozenset({"*"}),
|
|
492
|
+
}
|
|
493
|
+
PROCESS_TERMINATE_TIMEOUT_MS = 500
|
|
494
|
+
PROCESS_GROUP_POLL_MS = 25
|
|
495
|
+
COMMIT_BATCH_SIZE = 200
|
|
496
|
+
SEARCH_DEBOUNCE_MS = 250
|
|
497
|
+
# Typing pause after which a revision typed in a revision field is resolved: nothing is resolved while the user is
|
|
498
|
+
# still typing, so a revision that needs Git costs one process after the pause instead of one per key.
|
|
499
|
+
REVISION_INPUT_PAUSE_MS = 400
|
|
500
|
+
RELATIVE_DATE_REFRESH_MS = 60_000
|
|
501
|
+
AVATAR_SIDE = 64
|
|
502
|
+
AVATAR_INITIALS_FILL_RATIO = 0.85
|
|
503
|
+
AVATAR_TIMEOUT_MS = 5_000
|
|
504
|
+
AVATAR_ACTIVE_LIMIT = 4
|
|
505
|
+
AVATAR_PENDING_LIMIT = 128
|
|
506
|
+
AVATAR_RESPONSE_LIMIT_BYTES = 2 * 1024 * 1024
|
|
507
|
+
AVATAR_DECODED_SIDE_LIMIT = 1024
|
|
508
|
+
AVATAR_REQUEST_SIDE_LIMIT = 512
|
|
509
|
+
AVATAR_CACHE_ENTRY_LIMIT = 128
|
|
510
|
+
AVATAR_CACHE_LIMIT_BYTES = 16 * 1024 * 1024
|
|
511
|
+
AVATAR_CACHE_TTL_SECONDS = 3600
|
|
512
|
+
TOOL_START_TIMEOUT_MS = 5_000
|
|
513
|
+
MERGETOOL_START_TIMEOUT_MS = TOOL_START_TIMEOUT_MS
|
|
514
|
+
MERGETOOL_TERMINATE_TIMEOUT_MS = PROCESS_TERMINATE_TIMEOUT_MS
|
|
515
|
+
WINDOWS_EXCEPTION_STATUS_START = 0x80000000
|
|
516
|
+
SHELL_SCRIPT_EXTENSION_SET = frozenset({".bat", ".cmd"})
|
|
517
|
+
TOOL_START_ACTIVE_LIMIT = 4
|
|
518
|
+
DIFFTOOL_START_TIMEOUT_MS = 5_000
|
|
519
|
+
DIFFTOOL_ACTIVE_LIMIT = 4
|
|
520
|
+
TOOL_RESULT_POLL_MS = 25
|
|
521
|
+
FILE_EXPORT_BUFFER_CHUNKS = 128
|
|
522
|
+
FILE_EXPORT_ACTIVE_LIMIT = 2
|
|
523
|
+
COPY_CONFIRMATION_MS = 3_000
|
|
524
|
+
PARSING_BUDGET_MS = 5
|
|
525
|
+
STDERR_LIMIT_BYTES = 256 * 1024
|
|
526
|
+
COMMIT_LIMIT_BYTES = 8 * 1024 * 1024
|
|
527
|
+
MESSAGE_CACHE_LIMIT_BYTES = 32 * 1024 * 1024
|
|
528
|
+
DIFF_CACHE_LIMIT_BYTES = 16 * 1024 * 1024
|
|
529
|
+
DIFF_CACHE_ENTRY_LIMIT = 128
|
|
530
|
+
# Keep text layout small even for generated files; stream exact bytes separately for export.
|
|
531
|
+
FILE_PREVIEW_LIMIT_BYTES = 256 * 1024
|
|
532
|
+
FILE_LINE_LIMIT_CHARS = 4096
|
|
533
|
+
FILE_PARSE_BUDGET_MS = 5
|
|
534
|
+
FILE_CACHE_LIMIT_BYTES = 8 * 1024 * 1024
|
|
535
|
+
FILE_TEXT_BATCH_CHARS = 8192
|
|
536
|
+
SESSION_PROCESS_LIMIT = 2
|
|
537
|
+
GLOBAL_PROCESS_LIMIT = 4
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class ProcessPriority(IntEnum):
|
|
541
|
+
"""Order in which waiting Git processes take a free slot; a smaller value goes first.
|
|
542
|
+
|
|
543
|
+
A user action goes before the rest of an operation already under way, and both go before background work such as
|
|
544
|
+
the periodic refresh. The level only orders the waiting queue: it grants no permission, never changes a launch
|
|
545
|
+
profile and never interrupts a process that already runs.
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
USER = 0
|
|
549
|
+
CONTINUATION = 1
|
|
550
|
+
BACKGROUND = 2
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
SESSION_QUEUE_LIMIT = 64
|
|
554
|
+
GLOBAL_QUEUE_LIMIT = 256
|
|
555
|
+
PROCESS_CHUNK_BYTES = 64 * 1024
|
|
556
|
+
# The one byte asked beyond a validated file size, so that a file which grew while it was read shows it.
|
|
557
|
+
READ_SIZE_SENTINEL_BYTES = 1
|
|
558
|
+
PARSING_BACKLOG_LIMIT_BYTES = 8 * 1024 * 1024
|
|
559
|
+
SNAPSHOT_RETRY_LIMIT = 3
|
|
560
|
+
CAT_FILE_HEADER_LIMIT_BYTES = 256
|
|
561
|
+
COMMIT_HEADER_LIMIT_BYTES = 64 * 1024
|
|
562
|
+
HISTORY_REWRITE_SPOOL_LIMIT_BYTES = 4 * 1024 * 1024 * 1024
|
|
563
|
+
HISTORY_REWRITE_DIAGNOSTIC_LIMIT_CHARS = 2048
|
|
564
|
+
COMMIT_SUBJECT_LIMIT_CHARS = 512
|
|
565
|
+
COMMIT_PREVIEW_LIMIT_CHARS = 512
|
|
566
|
+
# Repositories the recent history keeps, the most recent first. The Open repository window and the repository switch
|
|
567
|
+
# menu show as many, and scroll past what fits.
|
|
568
|
+
RECENT_LIMIT = 100
|
|
569
|
+
# How long a recent's folder check may go without an answer before its row shows "checking": a folder on a local disk
|
|
570
|
+
# answers within a few milliseconds and so never shows it, and the recent list does not jump when the window opens.
|
|
571
|
+
RECENT_CHECKING_DISPLAY_DELAY_MS = 300
|
|
572
|
+
# How long after its check was requested a recent without an answer is shown as Unknown, even one still waiting behind a slow disk.
|
|
573
|
+
# A question blocked on a dead network mount cannot be cancelled, so the window stops waiting for it; its answer is still taken if it ever comes.
|
|
574
|
+
RECENT_CHECK_UNKNOWN_DELAY_MS = 30_000
|
|
575
|
+
# Parent folders of the last started clones offered by the clone form's Destination list.
|
|
576
|
+
CLONE_DESTINATION_LIMIT = 10
|
|
577
|
+
FIRST_ROW = 0
|
|
578
|
+
# Qt measures arc angles in sixteenths of a degree, starting at three o'clock.
|
|
579
|
+
QT_ANGLE_UNITS_PER_DEGREE = 16
|
|
580
|
+
# Shown in place of the commit subject by the branch forms when the selected commit could not be read.
|
|
581
|
+
COMMIT_METADATA_UNAVAILABLE = "Commit metadata unavailable"
|
|
582
|
+
# Text of the submit button of the forms that delete a branch or a tag.
|
|
583
|
+
DELETE_BUTTON_TEXT = "Delete"
|
|
584
|
+
# Title of both Delete branch windows and of the questions they ask.
|
|
585
|
+
DELETE_BRANCH_TITLE = "Delete branch"
|
|
586
|
+
# Text of the button that confirms a form whose action has no more precise name.
|
|
587
|
+
OK_BUTTON_TEXT = "OK"
|
|
588
|
+
# Shortcuts shared by the action that reacts to them and by the tooltip that cites them, in Qt's portable format.
|
|
589
|
+
FIND_SHORTCUT = "Ctrl+F"
|
|
590
|
+
STAGE_SHORTCUT = "S"
|
|
591
|
+
WELCOME_WIDTH = 720
|
|
592
|
+
REPOSITORY_WIDTH = 1440
|
|
593
|
+
REPOSITORY_HEIGHT = 900
|
|
594
|
+
LAYOUT_SPACING = 8
|
|
595
|
+
APPLICATION_NAME = "GitPane"
|
|
596
|
+
# The build reads this value from the text of this file without running it, so it stays a literal written as is:
|
|
597
|
+
# a computed value would make the build import the module instead.
|
|
598
|
+
APPLICATION_VERSION = "1.0.0"
|
|
599
|
+
# The line `gitpane --version` prints and the diagnostic log writes first.
|
|
600
|
+
APPLICATION_VERSION_LINE = APPLICATION_NAME + " " + APPLICATION_VERSION
|
|
601
|
+
# Bounds the helper that marks or writes the desktop shortcut (gio on Linux, PowerShell on Windows); it runs off the
|
|
602
|
+
# window's thread, so a slow helper never freezes Settings.
|
|
603
|
+
DESKTOP_SHORTCUT_HELPER_TIMEOUT_MS = 30_000
|
|
604
|
+
# Windows groups taskbar buttons by this identifier. Company.Product format, without a version so an update keeps the same button.
|
|
605
|
+
APPLICATION_USER_MODEL_ID = "ThongvanAlexis.GitPane"
|
|
606
|
+
# macOS knows the desktop bundle of GitPane by this reverse-DNS name. The Windows identifier above is not reused: it is not in that form.
|
|
607
|
+
MACOS_BUNDLE_IDENTIFIER = "io.github.thongvanalexis.gitpane"
|
|
608
|
+
WINDOWS_PLATFORM = "win32"
|
|
609
|
+
LINUX_PLATFORM = "linux"
|
|
610
|
+
MACOS_PLATFORM = "darwin"
|
|
611
|
+
# Linux's process folder, read by the process supervisor (process states) and by the keyring read (the program a process runs).
|
|
612
|
+
PROC_DIR = "/proc"
|
|
613
|
+
# The Windows environment variable that names the Windows folder, read by the terminal search and the SSH client search.
|
|
614
|
+
SYSTEM_ROOT_ENVIRONMENT_NAME = "SystemRoot"
|
|
615
|
+
# The Unicode form macOS gives the names it creates and Qt for macOS gives every argument: accents decomposed into a letter and a combining mark.
|
|
616
|
+
DECOMPOSED_UNICODE_FORM = "NFD"
|
|
617
|
+
# The macOS command that hands a file, a folder or an application to LaunchServices (Finder, Terminal, default editor).
|
|
618
|
+
MACOS_OPEN_FILENAME = "/usr/bin/open"
|
|
619
|
+
# Start of every HTTPS URL and account origin; shared by the account service and the reader of Git's refusal lines.
|
|
620
|
+
HTTPS_PREFIX = "https://"
|
|
621
|
+
WINDOWS_KDIFF3_FILENAME = "C:/Program Files/KDiff3/kdiff3.exe"
|
|
622
|
+
# Where the distribution packages install KDiff3.
|
|
623
|
+
LINUX_KDIFF3_FILENAME = "/usr/bin/kdiff3"
|
|
624
|
+
# Standard KDiff3 installation of each system, keyed by sys.platform, offered in Settings when no comparison tool is saved.
|
|
625
|
+
# macOS has no fixed file: its KDiff3 is an application bundle, searched through KDIFF3_BUNDLE_BASENAMES_BY_PLATFORM below.
|
|
626
|
+
# A system missing from the table gets no suggestion rather than another system's location.
|
|
627
|
+
KDIFF3_SUGGESTED_FILENAMES_BY_PLATFORM: dict[str, tuple[str, ...]] = {
|
|
628
|
+
WINDOWS_PLATFORM: (WINDOWS_KDIFF3_FILENAME,),
|
|
629
|
+
LINUX_PLATFORM: (LINUX_KDIFF3_FILENAME,),
|
|
630
|
+
MACOS_PLATFORM: (),
|
|
631
|
+
}
|
|
632
|
+
# Homebrew's kdiff3 cask and KDE's disk image both install this application folder.
|
|
633
|
+
KDIFF3_BUNDLE_BASENAME = "kdiff3.app"
|
|
634
|
+
# KDiff3 application folders of each system, keyed by sys.platform, searched in the applications folders after the fixed
|
|
635
|
+
# files above. GitPane offers the program the bundle declares, the same one Browse… gives on that folder, never the script
|
|
636
|
+
# the cask adds to Homebrew's bin folder. A system missing from the table gets no suggestion.
|
|
637
|
+
KDIFF3_BUNDLE_BASENAMES_BY_PLATFORM: dict[str, tuple[str, ...]] = {
|
|
638
|
+
WINDOWS_PLATFORM: (),
|
|
639
|
+
LINUX_PLATFORM: (),
|
|
640
|
+
MACOS_PLATFORM: (KDIFF3_BUNDLE_BASENAME,),
|
|
641
|
+
}
|
|
642
|
+
# KDE's download folder of the stable KDiff3 releases, which holds the Windows installer (.exe).
|
|
643
|
+
KDIFF3_DOWNLOAD_URL = "https://download.kde.org/stable/kdiff3/"
|
|
644
|
+
# How to install KDiff3 on Windows and macOS, keyed by sys.platform: shown under the tool fields of Settings while no
|
|
645
|
+
# KDiff3 is found, and after the message of a comparison or a conflict resolution refused for a missing tool. Linux has
|
|
646
|
+
# one line per distribution, in gitpane/tool/kdiff3_install.py. Any other system gets no line rather than another
|
|
647
|
+
# system's.
|
|
648
|
+
KDIFF3_INSTALL_LINE_BY_PLATFORM: dict[str, str] = {
|
|
649
|
+
WINDOWS_PLATFORM: "To install KDiff3, download its Windows installer from " + KDIFF3_DOWNLOAD_URL,
|
|
650
|
+
MACOS_PLATFORM: "To install KDiff3, run: brew install --cask kdiff3",
|
|
651
|
+
}
|
|
652
|
+
# Oldest Git Credential Manager that accepts every setting GitPane passes to it (the "none" store included) and that
|
|
653
|
+
# carries the fix for CVE-2024-50338; an older one is refused with its version so the user knows what to update.
|
|
654
|
+
GCM_MINIMUM_VERSION = (2, 6, 1)
|
|
655
|
+
# Longest wait for `git-credential-manager --version`: the first start of .NET on Windows, antivirus scan included, can
|
|
656
|
+
# take several seconds.
|
|
657
|
+
GCM_VERSION_TIMEOUT_MS = 10_000
|
|
658
|
+
# Start of the names of the environment variables the Git Credential Manager reads as its own settings. Every place
|
|
659
|
+
# that drops the user's settings before starting it, or before starting Git that starts it, reads this one value.
|
|
660
|
+
GCM_ENVIRONMENT_PREFIX = "GCM_"
|
|
661
|
+
GCM_RELEASES_URL = "https://github.com/git-ecosystem/git-credential-manager/releases/latest"
|
|
662
|
+
# Version of the official .tar.gz archive the Linux install lines download: the latest stable release when these lines
|
|
663
|
+
# were written, at least GCM_MINIMUM_VERSION. Raised by hand when a newer release is worth giving.
|
|
664
|
+
GCM_ARCHIVE_VERSION = "3.0.2"
|
|
665
|
+
GIT_FOR_WINDOWS_DOWNLOAD_URL = "https://git-scm.com/downloads/win"
|
|
666
|
+
# Where a Mac without Homebrew gets it before installing the credential manager with it.
|
|
667
|
+
HOMEBREW_INSTALL_URL = "https://brew.sh"
|
|
668
|
+
# First line of the "credential manager missing" message, the same on every system; the install lines of the table follow it.
|
|
669
|
+
GCM_REQUIRED_TEXT = "A git credential manager is required to push/fetch"
|
|
670
|
+
GCM_WINDOWS_BASENAME = "git-credential-manager.exe"
|
|
671
|
+
GCM_BASENAME = "git-credential-manager"
|
|
672
|
+
# Where the .pkg installer of the Homebrew cask, the official .deb and the archive install lines all put the credential
|
|
673
|
+
# manager, on Apple silicon and Intel alike. An application started from the Finder or from the desktop menu does not
|
|
674
|
+
# always have /usr/local/bin in its PATH, and the archive lines put no link there, so this location is searched after
|
|
675
|
+
# the PATH. posixpath, because the path is POSIX on both systems that use it, whatever system runs the tests.
|
|
676
|
+
GCM_CORE_DIR = "/usr/local/share/gcm-core"
|
|
677
|
+
GCM_CORE_FILENAME = posixpath.join(GCM_CORE_DIR, GCM_BASENAME)
|
|
678
|
+
# Install line of the macOS entry, given both for a missing credential manager and for one that is installed but unusable.
|
|
679
|
+
MACOS_GCM_INSTALL_LINE = " - run : brew install --cask git-credential-manager"
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@dataclass(frozen=True)
|
|
683
|
+
class CredentialManagerPlatform:
|
|
684
|
+
"""Everything about the Git Credential Manager that differs from one system to another.
|
|
685
|
+
|
|
686
|
+
Its file name, whether the Git installation in use is searched for it, the fixed locations searched after the
|
|
687
|
+
PATH, the secure store that keeps GitPane's accounts, and the install lines shown when it is refused, which may
|
|
688
|
+
depend on why it is refused.
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
executable_basename: str
|
|
692
|
+
searches_git_installation: bool
|
|
693
|
+
fixed_filenames: tuple[str, ...]
|
|
694
|
+
credential_store: str
|
|
695
|
+
# Lines for a credential manager that is installed but unusable (unreadable version, does not start), and for any
|
|
696
|
+
# refusal that has no lines of its own below.
|
|
697
|
+
install_lines: tuple[str, ...]
|
|
698
|
+
# A missing credential manager and one that is too old are not installed the same way everywhere (install versus
|
|
699
|
+
# upgrade); empty means the same lines as install_lines.
|
|
700
|
+
missing_install_lines: tuple[str, ...] = ()
|
|
701
|
+
too_old_install_lines: tuple[str, ...] = ()
|
|
702
|
+
# True when the lines depend on the distribution and the processor, written by the module of the Linux install
|
|
703
|
+
# lines; install_lines then stays empty.
|
|
704
|
+
install_lines_by_distribution: bool = False
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
# Keyed by sys.platform. A system missing from the table is refused as unsupported, never served with another system's
|
|
708
|
+
# location, store or install lines.
|
|
709
|
+
CREDENTIAL_MANAGER_PLATFORM_BY_NAME: dict[str, CredentialManagerPlatform] = {
|
|
710
|
+
WINDOWS_PLATFORM: CredentialManagerPlatform(
|
|
711
|
+
executable_basename=GCM_WINDOWS_BASENAME, searches_git_installation=True, fixed_filenames=(), credential_store="wincredman",
|
|
712
|
+
install_lines=(' - reinstall Git for Windows with "Git Credential Manager" checked', " - You can get it there in case you forgot : " + GIT_FOR_WINDOWS_DOWNLOAD_URL),
|
|
713
|
+
),
|
|
714
|
+
LINUX_PLATFORM: CredentialManagerPlatform(
|
|
715
|
+
executable_basename=GCM_BASENAME, searches_git_installation=False, fixed_filenames=(GCM_CORE_FILENAME,), credential_store="secretservice",
|
|
716
|
+
install_lines=(), install_lines_by_distribution=True,
|
|
717
|
+
),
|
|
718
|
+
MACOS_PLATFORM: CredentialManagerPlatform(
|
|
719
|
+
executable_basename=GCM_BASENAME, searches_git_installation=False, fixed_filenames=(GCM_CORE_FILENAME,), credential_store="keychain",
|
|
720
|
+
install_lines=(MACOS_GCM_INSTALL_LINE,),
|
|
721
|
+
missing_install_lines=(MACOS_GCM_INSTALL_LINE, " - no Homebrew yet? install it first : " + HOMEBREW_INSTALL_URL),
|
|
722
|
+
too_old_install_lines=(" - run : brew upgrade --cask git-credential-manager",),
|
|
723
|
+
),
|
|
724
|
+
}
|
|
725
|
+
SPACING_XS, SPACING_SM, SPACING_MD, SPACING_LG, SPACING_XL, SPACING_2XL, SPACING_3XL = 4, 8, 16, 24, 32, 48, 64
|
|
726
|
+
SCREEN_MARGIN = SPACING_MD
|
|
727
|
+
# Rows the repository switch list shows at once, like an ordinary dropdown list; it scrolls past them.
|
|
728
|
+
REPOSITORY_LIST_MAX_VISIBLE_ROW_COUNT = 20
|
|
729
|
+
# Gap added after the number and after the name in the repository switch list; Qt already counts a small style margin in
|
|
730
|
+
# each measured column.
|
|
731
|
+
REPOSITORY_LIST_COLUMN_GAP = SPACING_SM
|
|
732
|
+
# The Open repository buttons sit in the band under the form, so the form itself is short: the minimum height stays
|
|
733
|
+
# under the height the form wants with a few recent repositories, so the window still grows with them.
|
|
734
|
+
WELCOME_MIN_WIDTH, WELCOME_MIN_HEIGHT = 480, 240
|
|
735
|
+
# The Open repository form grows with its recent repositories up to this share of the usable screen height; past it, the form scrolls.
|
|
736
|
+
WELCOME_MAX_SCREEN_RATIO = 0.8
|
|
737
|
+
REPOSITORY_MIN_WIDTH, REPOSITORY_MIN_HEIGHT = 800, 560
|
|
738
|
+
SETTING_WIDTH, SETTING_HEIGHT = 800, 600
|
|
739
|
+
SETTING_MIN_WIDTH, SETTING_MIN_HEIGHT = 480, 320
|
|
740
|
+
ERROR_WIDTH, ERROR_HEIGHT = 640, 320
|
|
741
|
+
# Share of the usable screen height the diagnostic window never goes past, title bar included; past it, its details scroll.
|
|
742
|
+
ERROR_MAX_SCREEN_HEIGHT_RATIO = 0.8
|
|
743
|
+
# Share of the room left to the cause and the details that the cause may take when the diagnostic window is at its height cap.
|
|
744
|
+
ERROR_SUMMARY_MAX_SHARE = 0.5
|
|
745
|
+
# Client-area width of the Git Extensions Cherry pick commit and Revert commit windows (615 px with their borders); the
|
|
746
|
+
# height follows the content. The revision chooser keeps the same starting size.
|
|
747
|
+
REPLAY_DIALOG_WIDTH = 613
|
|
748
|
+
REPLAY_DIALOG_HEIGHT = 360
|
|
749
|
+
NAVIGATION_WIDTH, NAVIGATION_MIN_WIDTH = 240, 160
|
|
750
|
+
HISTORY_MIN_HEIGHT, DETAIL_MIN_HEIGHT = 192, 200
|
|
751
|
+
HISTORY_STRETCH, DETAIL_STRETCH = 1, 1
|
|
752
|
+
HISTORY_ROW_MIN_HEIGHT = 24
|
|
753
|
+
# The package and the folder where GitPane keeps the images and the font it ships, read through importlib.resources.
|
|
754
|
+
# Every reader takes them from here, so moving the resources means changing one place.
|
|
755
|
+
UI_RESOURCE_PACKAGE_NAME = "gitpane.ui"
|
|
756
|
+
UI_RESOURCE_DIR_NAME = "resource"
|
|
757
|
+
# Names of the packaged application pictures that the icon drawing tool writes into that folder, read by the desktop shortcut.
|
|
758
|
+
APPLICATION_ICON_FILE_NAME_PREFIX = "GitPane-"
|
|
759
|
+
APPLICATION_ICON_PNG_EXTENSION = ".png"
|
|
760
|
+
APPLICATION_ICON_ICO_BASENAME = "GitPane.ico"
|
|
761
|
+
ICON_SIDE, GRAPH_NODE_SIDE, GRAPH_LANE_WIDTH = 16, 8, 16
|
|
762
|
+
PARENT_MARKER_FILL = "#e87500"
|
|
763
|
+
PARENT_MARKER_BORDER = "#c73939"
|
|
764
|
+
PARENT_MARKER_BACKGROUND = "#ffffff"
|
|
765
|
+
PARENT_MARKER_BORDER_WIDTH = 1
|
|
766
|
+
PARENT_MARKER_INSET = 2
|
|
767
|
+
PARENT_MARKER_SHOULDER_RATIO = 0.55
|
|
768
|
+
PARENT_MARKER_SHAFT_RATIO = 0.3
|
|
769
|
+
GRAPH_MESSAGE_MIN_CHARACTERS = 12
|
|
770
|
+
TERMINAL_CONTROL_HEIGHT = 32
|
|
771
|
+
# Arrow of the toolbar menu buttons, measured at 100 % on a Git Extensions toolbar capture: a filled triangle whose
|
|
772
|
+
# rows are 7, 5, 3 and 1 pixels wide, 9 pixels after the end of the button text.
|
|
773
|
+
MENU_ARROW_WIDTH, MENU_ARROW_HEIGHT = 7, 4
|
|
774
|
+
MENU_ARROW_TEXT_GAP = 9
|
|
775
|
+
MENU_ARROW_AREA_WIDTH = 11
|
|
776
|
+
MENU_SEPARATOR_WIDTH = 1
|
|
777
|
+
# Right padding of the button content, tuned so the measured gap equals MENU_ARROW_TEXT_GAP with the fonts of Windows and
|
|
778
|
+
# Linux: Qt adds two spaces of the button font after the text and the triangle is centred in the menu area, so the gap is
|
|
779
|
+
# larger than this padding and grows with the font's space. The pixel test checks the gap this layout gives in the button's
|
|
780
|
+
# font on every system, and that it equals MENU_ARROW_TEXT_GAP on Windows and Linux (macOS buttons get a smaller font).
|
|
781
|
+
MENU_ARROW_TEXT_PADDING = 2
|
|
782
|
+
# Top bar of the repository window: the repository button gives way to the filter and commit hash fields, which
|
|
783
|
+
# keep at least these widths. The button shows the whole path up to its maximum width and elides it in the middle below
|
|
784
|
+
# that, down to its minimum text width. Starting values, to be judged on screen captures at 100 %.
|
|
785
|
+
REPOSITORY_BUTTON_MAX_WIDTH = 480
|
|
786
|
+
REPOSITORY_BUTTON_MIN_TEXT_WIDTH = 64
|
|
787
|
+
# Narrower than the hash field since the filter mode button, wider than the label it replaced, sits beside it: at the
|
|
788
|
+
# minimum window width the repository button still shows the repository name with its separator.
|
|
789
|
+
FILTER_FIELD_MIN_WIDTH = 104
|
|
790
|
+
HASH_FIELD_MIN_WIDTH = 120
|
|
791
|
+
TOP_BAR_FIELD_STRETCH = 1
|
|
792
|
+
HEADING_FONT_RATIO, DISPLAY_FONT_RATIO = 1.25, 1.75
|
|
793
|
+
MESSAGE_LINE_HEIGHT_PERCENT = 150
|
|
794
|
+
DIFF_FRAGMENT_LIMIT_CHARS = 4096
|
|
795
|
+
DIFF_SYNTAX_LIMIT_CHARS = 16384
|
|
796
|
+
DIFF_ICON_PADDING_RATIO = 0.2
|
|
797
|
+
DIFF_ICON_STROKE_RATIO = 0.14
|
|
798
|
+
DIFF_ICON_RENAME_BADGE_AREA_RATIO = 0.25
|
|
799
|
+
DIFF_NUMBER_COLUMN_COUNT = 2
|
|
800
|
+
DIFF_FILE_STRETCH, DIFF_TEXT_STRETCH = 1, 3
|
|
801
|
+
# The message splitter has a third pane for the emoji keyboard, but its preference keeps two sizes: upper (diff and keyboard) and message.
|
|
802
|
+
STAGING_MESSAGE_PROPORTION_KEY = "staging_message_proportions"
|
|
803
|
+
STAGING_PROPORTION_BY_KEY: dict[str, tuple[int, int]] = {
|
|
804
|
+
"staging_file_proportions": (DIFF_FILE_STRETCH, DIFF_TEXT_STRETCH),
|
|
805
|
+
"staging_list_proportions": (1, 1),
|
|
806
|
+
STAGING_MESSAGE_PROPORTION_KEY: (DIFF_TEXT_STRETCH, DIFF_FILE_STRETCH),
|
|
807
|
+
}
|
|
808
|
+
# Duration of the slide that opens or closes the staging emoji keyboard.
|
|
809
|
+
EMOJI_GRID_ANIMATION_MS = 150
|
|
810
|
+
# Colour emoji glyphs ignore the palette's disabled colour, so a disabled keyboard is dimmed to this opacity to look disabled.
|
|
811
|
+
EMOJI_GRID_DISABLED_OPACITY = 0.4
|
|
812
|
+
EMOJI_POINT_SIZE = 18
|
|
813
|
+
EMOJI_BUTTON_MIN_SIDE = SPACING_XL
|
|
814
|
+
# Combining mark buttons show the mark between two emoji, so they are this many symbol buttons wide.
|
|
815
|
+
EMOJI_MARK_BUTTON_WIDTH_RATIO = 2
|
|
816
|
+
# Shared label of the staging button, the Tools menu action and the title of the floating keyboard window.
|
|
817
|
+
EMOJI_KEYBOARD_TITLE = "Emoji keyboard"
|
|
818
|
+
# How long the floating keyboard's copy bubble stays up; copied hashes keep their own, longer status confirmation.
|
|
819
|
+
EMOJI_COPY_TOAST_MS = 1_200
|
|
820
|
+
COPIED_TO_CLIPBOARD_TEXT = "Copied to clipboard"
|
|
821
|
+
# The floating keyboard first opens at this share of the usable screen width, then as tall as its laid-out grid needs to
|
|
822
|
+
# show every symbol at that width; it only grows wider when that height does not fit on the screen.
|
|
823
|
+
EMOJI_WINDOW_SCREEN_WIDTH_SHARE = 0.4
|
|
824
|
+
EMOJI_WINDOW_MIN_WIDTH, EMOJI_WINDOW_MIN_HEIGHT = 240, 160
|
|
825
|
+
EMOJI_WINDOW_GEOMETRY_PREFERENCE_KEY = "emoji_window_geometry"
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
SUBMODULE_SUMMARY_OUTPUT_LIMIT_BYTES = 1024 * 1024
|
|
829
|
+
SUBMODULE_SUMMARY_COMMIT_LIMIT = 100
|
|
830
|
+
SUBMODULE_SUMMARY_DIFFERENCE_LIMIT = 500
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def round_up_to_spacing(value: int) -> int:
|
|
834
|
+
"""Round a size up to the next multiple of the smallest spacing, so that controls stay on the spacing grid."""
|
|
835
|
+
return ((value + SPACING_XS - 1) // SPACING_XS) * SPACING_XS
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def application_icon_png_basename(side: int) -> str:
|
|
839
|
+
"""Return the name of the packaged PNG picture of the application logo drawn ``side`` pixels wide."""
|
|
840
|
+
return APPLICATION_ICON_FILE_NAME_PREFIX + str(side) + APPLICATION_ICON_PNG_EXTENSION
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def control_height(font_height: int) -> int:
|
|
844
|
+
"""Round a readable control height upward without scaling logical pixels twice."""
|
|
845
|
+
return round_up_to_spacing(max(SPACING_XL, font_height + SPACING_SM))
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def emoji_button_side(font_height: int) -> int:
|
|
849
|
+
"""Return the side of a square emoji button: the symbol plus a small margin, never below the minimum, on the spacing grid."""
|
|
850
|
+
return round_up_to_spacing(max(EMOJI_BUTTON_MIN_SIDE, font_height + SPACING_XS))
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def history_row_height(font_height: int) -> int:
|
|
854
|
+
"""Keep the history compact while giving enlarged system fonts room to breathe."""
|
|
855
|
+
return max(HISTORY_ROW_MIN_HEIGHT, font_height + SPACING_SM)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def keep_selection_visible(widget: QWidget) -> bool:
|
|
859
|
+
"""Retain the system highlight while another control in the window owns focus; answer whether the palette had to change.
|
|
860
|
+
|
|
861
|
+
Starts from the widget's own palette, which pins only the colors set here: a copy of the application palette would
|
|
862
|
+
also pin the window and field colors GitPane lays in a light theme, and the widget would keep them after a switch
|
|
863
|
+
to a dark theme. A palette that already has them is left alone, so no palette change is raised for nothing.
|
|
864
|
+
"""
|
|
865
|
+
from PySide6.QtGui import QPalette
|
|
866
|
+
|
|
867
|
+
palette = widget.palette()
|
|
868
|
+
changed = False
|
|
869
|
+
for role in (QPalette.ColorRole.Highlight, QPalette.ColorRole.HighlightedText):
|
|
870
|
+
active_color = palette.color(QPalette.ColorGroup.Active, role)
|
|
871
|
+
if palette.color(QPalette.ColorGroup.Inactive, role) == active_color and palette.isBrushSet(QPalette.ColorGroup.Inactive, role):
|
|
872
|
+
continue
|
|
873
|
+
palette.setColor(QPalette.ColorGroup.Inactive, role, active_color)
|
|
874
|
+
changed = True
|
|
875
|
+
if changed:
|
|
876
|
+
widget.setPalette(palette)
|
|
877
|
+
return changed
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def fit_window(widget: QWidget, minimum_width: int, minimum_height: int, available: QRect | None = None, *, platform_name: str = "") -> None:
|
|
881
|
+
"""Keep the frame inside usable screen space, relaxing nominal minima on small screens.
|
|
882
|
+
|
|
883
|
+
The size is fitted on every platform. Where the compositor places windows itself, the frame is not moved back
|
|
884
|
+
inside the area: the compositor ignores the position. ``platform_name`` is Qt's platform name, read from Qt when not
|
|
885
|
+
given.
|
|
886
|
+
"""
|
|
887
|
+
from PySide6.QtCore import QPoint
|
|
888
|
+
|
|
889
|
+
available = (available if available is not None else widget.screen().availableGeometry()).adjusted(SCREEN_MARGIN, SCREEN_MARGIN, -SCREEN_MARGIN, -SCREEN_MARGIN)
|
|
890
|
+
decoration = known_frame_margins(widget, platform_name)
|
|
891
|
+
width = max(1, available.width() - decoration.left() - decoration.right())
|
|
892
|
+
height = max(1, available.height() - decoration.top() - decoration.bottom())
|
|
893
|
+
widget.setMinimumSize(min(minimum_width, width), min(minimum_height, height))
|
|
894
|
+
widget.resize(min(widget.width(), width), min(widget.height(), height))
|
|
895
|
+
if not window_placement(platform_name).requests_position:
|
|
896
|
+
return
|
|
897
|
+
margins = known_frame_margins(widget, platform_name)
|
|
898
|
+
position = frame_position_inside(widget.geometry().marginsAdded(margins), available)
|
|
899
|
+
# While the widget's copy of its frame is still its client area, Qt places the client area at the position it is
|
|
900
|
+
# given and the window system adds the title bar above it (see center_window_at): move it by the margins only the
|
|
901
|
+
# native window knows, so the title bar stays inside the area.
|
|
902
|
+
if widget.frameGeometry() == widget.geometry():
|
|
903
|
+
position += QPoint(margins.left(), margins.top())
|
|
904
|
+
widget.move(position)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def known_frame_margins(window: QWidget, platform_name: str = "") -> QMargins:
|
|
908
|
+
"""Return the title bar and borders the window system already gave a window, or no margin while it has given none.
|
|
909
|
+
|
|
910
|
+
The widget keeps its own copy of its frame, refreshed only when the window is moved or resized. On the platforms of
|
|
911
|
+
NATIVE_FRAME_MARGIN_PLATFORM_NAME_SET, a title bar created after that (drawn by Qt itself under Wayland once the
|
|
912
|
+
window is shown, or built late by AppKit on macOS) leaves the copy equal to the client area while the native window
|
|
913
|
+
already knows its margins, so those are read then. Elsewhere only the copy counts, and a window not decorated yet
|
|
914
|
+
reports no margin. ``platform_name`` is Qt's platform name, read from Qt when not given.
|
|
915
|
+
"""
|
|
916
|
+
from PySide6.QtCore import QMargins
|
|
917
|
+
from PySide6.QtGui import QGuiApplication
|
|
918
|
+
|
|
919
|
+
frame = window.frameGeometry()
|
|
920
|
+
client = window.geometry()
|
|
921
|
+
if frame != client:
|
|
922
|
+
return QMargins(max(0, client.left() - frame.left()), max(0, client.top() - frame.top()), max(0, frame.right() - client.right()),
|
|
923
|
+
max(0, frame.bottom() - client.bottom()))
|
|
924
|
+
if (platform_name or QGuiApplication.platformName()) not in NATIVE_FRAME_MARGIN_PLATFORM_NAME_SET:
|
|
925
|
+
return QMargins()
|
|
926
|
+
native_window = window.windowHandle()
|
|
927
|
+
if native_window is None:
|
|
928
|
+
return QMargins()
|
|
929
|
+
return native_window.frameMargins()
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def frame_position_inside(frame: QRect, available: QRect) -> QPoint:
|
|
933
|
+
"""Return the top-left corner that keeps a window frame inside the usable area, moving it as little as possible."""
|
|
934
|
+
from PySide6.QtCore import QPoint
|
|
935
|
+
|
|
936
|
+
left = min(max(frame.left(), available.left()), available.right() - frame.width() + 1)
|
|
937
|
+
top = min(max(frame.top(), available.top()), available.bottom() - frame.height() + 1)
|
|
938
|
+
return QPoint(left, top)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
# Names of Qt's platform plugins, as QGuiApplication.platformName() gives them; not the sys.platform values above.
|
|
942
|
+
# Qt's name for the macOS window system.
|
|
943
|
+
MACOS_PLATFORM_NAME = "cocoa"
|
|
944
|
+
WINDOWS_PLATFORM_NAME = "windows"
|
|
945
|
+
# Qt's name for the X11 window system.
|
|
946
|
+
X11_PLATFORM_NAME = "xcb"
|
|
947
|
+
# Qt's name for the Wayland window system.
|
|
948
|
+
WAYLAND_PLATFORM_NAME = "wayland"
|
|
949
|
+
# Platforms where the widget's copy of a window's frame can stay its client area while the native window already knows
|
|
950
|
+
# its title bar: AppKit can build the frame after Qt took the window's first geometry, and under Wayland Qt draws the
|
|
951
|
+
# title bar itself once the window is shown. There the native window's margins are read; elsewhere only the widget's
|
|
952
|
+
# copy counts, and while it is empty the title bar of a decorated window is borrowed.
|
|
953
|
+
NATIVE_FRAME_MARGIN_PLATFORM_NAME_SET = frozenset({MACOS_PLATFORM_NAME, WAYLAND_PLATFORM_NAME})
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
class WindowPlacement(Enum):
|
|
957
|
+
"""Who places a new GitPane window on one Qt platform."""
|
|
958
|
+
|
|
959
|
+
CENTRED_ON_ANCHOR = "centred-on-anchor"
|
|
960
|
+
LEFT_TO_COMPOSITOR = "left-to-compositor"
|
|
961
|
+
|
|
962
|
+
@property
|
|
963
|
+
def requests_position(self) -> bool:
|
|
964
|
+
"""Tell whether GitPane moves its windows itself; a compositor that places them ignores any position GitPane asks for."""
|
|
965
|
+
return self is WindowPlacement.CENTRED_ON_ANCHOR
|
|
966
|
+
|
|
967
|
+
@property
|
|
968
|
+
def size_waits_for_compositor_answer(self) -> bool:
|
|
969
|
+
"""Tell whether a size asked just after a window is shown must wait for the compositor's answer to that show.
|
|
970
|
+
|
|
971
|
+
A compositor that places the windows answers the first show with a size of its own, the one it knew at that
|
|
972
|
+
moment, and the toolkit applies it: a size asked before that answer is overwritten for good.
|
|
973
|
+
"""
|
|
974
|
+
return self is WindowPlacement.LEFT_TO_COMPOSITOR
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
DEFAULT_WINDOW_PLACEMENT = WindowPlacement.CENTRED_ON_ANCHOR
|
|
978
|
+
# Under Wayland the compositor places every top-level window and Qt drops the position a program asks for, so GitPane
|
|
979
|
+
# only sizes its windows there and asks for no position.
|
|
980
|
+
WINDOW_PLACEMENT_BY_PLATFORM_NAME: dict[str, WindowPlacement] = {
|
|
981
|
+
WAYLAND_PLATFORM_NAME: WindowPlacement.LEFT_TO_COMPOSITOR,
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def window_placement(platform_name: str = "") -> WindowPlacement:
|
|
986
|
+
"""Return who places GitPane's windows on a Qt platform, so the placement code only asks for positions where they are taken.
|
|
987
|
+
|
|
988
|
+
``platform_name`` is Qt's platform name, read from Qt when not given; a platform missing from the table keeps the
|
|
989
|
+
default, where GitPane centres its windows itself.
|
|
990
|
+
"""
|
|
991
|
+
from PySide6.QtGui import QGuiApplication
|
|
992
|
+
|
|
993
|
+
return WINDOW_PLACEMENT_BY_PLATFORM_NAME.get(platform_name or QGuiApplication.platformName(), DEFAULT_WINDOW_PLACEMENT)
|
|
994
|
+
|
|
995
|
+
# Style GitPane runs on every system, and the legacy Windows default style it replaces there.
|
|
996
|
+
FUSION_STYLE_NAME = "Fusion"
|
|
997
|
+
LEGACY_WINDOWS_STYLE_NAME = "windowsvista"
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def unknown_decoration_estimate(window: QWidget, decoration_reference: QWidget | None, platform_name: str = "") -> QMargins:
|
|
1001
|
+
"""Estimate the title bar and borders that a window's frame does not count yet, so placing the frame leaves room for them.
|
|
1002
|
+
|
|
1003
|
+
Windows gives Qt a window's frame before the window is shown; an X11 window manager only adds it once the window is
|
|
1004
|
+
mapped, so until then the frame equals the client area and the title bar is borrowed from a decorated window. On
|
|
1005
|
+
macOS the native window knows its title bar from its creation, but Qt's copy of it in the widget can stay empty: a
|
|
1006
|
+
window that got its first geometry before AppKit had built its frame keeps a frame equal to its client area until
|
|
1007
|
+
the window system next moves or resizes it. Centring that frame puts the client area where the frame should be,
|
|
1008
|
+
and macOS then adds the title bar above it. Under Wayland Qt draws the title bar itself after the window is shown,
|
|
1009
|
+
with the same stale copy. So on macOS and under Wayland the native window's own margins are used; nothing is ever
|
|
1010
|
+
borrowed from another window on macOS, while under Wayland a window not decorated yet still borrows. The estimate
|
|
1011
|
+
is empty whenever the frame is already known or the window has no decoration. ``platform_name`` is Qt's platform
|
|
1012
|
+
name, read from Qt when not given.
|
|
1013
|
+
"""
|
|
1014
|
+
from PySide6.QtCore import QMargins, Qt
|
|
1015
|
+
from PySide6.QtGui import QGuiApplication
|
|
1016
|
+
from shiboken6 import isValid
|
|
1017
|
+
|
|
1018
|
+
if window.frameGeometry() != window.geometry() or window.windowFlags() & Qt.WindowType.FramelessWindowHint:
|
|
1019
|
+
return QMargins()
|
|
1020
|
+
platform_name = platform_name or QGuiApplication.platformName()
|
|
1021
|
+
if platform_name in NATIVE_FRAME_MARGIN_PLATFORM_NAME_SET:
|
|
1022
|
+
native_margins = known_frame_margins(window, platform_name)
|
|
1023
|
+
# The macOS native window knows its title bar from its creation: empty margins there mean nothing to add yet,
|
|
1024
|
+
# and a title bar borrowed from another window would place the window too low.
|
|
1025
|
+
if not native_margins.isNull() or platform_name == MACOS_PLATFORM_NAME:
|
|
1026
|
+
return native_margins
|
|
1027
|
+
if decoration_reference is None or not isValid(decoration_reference):
|
|
1028
|
+
return QMargins()
|
|
1029
|
+
reference_frame = decoration_reference.frameGeometry()
|
|
1030
|
+
reference_client = decoration_reference.geometry()
|
|
1031
|
+
return QMargins(max(0, reference_client.left() - reference_frame.left()), max(0, reference_client.top() - reference_frame.top()),
|
|
1032
|
+
max(0, reference_frame.right() - reference_client.right()), max(0, reference_frame.bottom() - reference_client.bottom()))
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def uncounted_decoration_estimate(window: QWidget, decoration_reference: QWidget | None, platform_name: str = "") -> QMargins:
|
|
1036
|
+
"""Estimate the title bar and borders that ``known_frame_margins`` does not count yet, to take off a usable area a size is fitted in.
|
|
1037
|
+
|
|
1038
|
+
The size fits (``fit_window``, the largest client size) already take off the margins the window system gave,
|
|
1039
|
+
including those only the native window knows. Taking the full estimate off the area as well would count such a
|
|
1040
|
+
title bar twice and leave the window shorter by its height, so the estimate only applies while no margin is known.
|
|
1041
|
+
"""
|
|
1042
|
+
from PySide6.QtCore import QMargins
|
|
1043
|
+
|
|
1044
|
+
if not known_frame_margins(window, platform_name).isNull():
|
|
1045
|
+
return QMargins()
|
|
1046
|
+
return unknown_decoration_estimate(window, decoration_reference, platform_name)
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def center_window_at(window: QWidget, centre: QPoint, screen: QScreen, decoration_reference: QWidget | None = None, *, platform_name: str = "") -> None:
|
|
1050
|
+
"""Put the window's frame centre on a point, then pull the frame back inside the screen's usable area if it crosses an edge.
|
|
1051
|
+
|
|
1052
|
+
Frames are compared, not client areas, so the title bar counts as part of the window; the current frame size is used.
|
|
1053
|
+
While the window system has not told the frame yet (X11 before the first map), ``decoration_reference`` lends its
|
|
1054
|
+
title bar and borders: Qt then places the client area at the position it is given and the window manager adds the
|
|
1055
|
+
title bar above it, so the client area is moved down by that estimate. Where the compositor places windows itself,
|
|
1056
|
+
nothing is moved: it would ignore the position. ``platform_name`` is Qt's platform name, read from Qt when not given.
|
|
1057
|
+
"""
|
|
1058
|
+
from PySide6.QtCore import QPoint
|
|
1059
|
+
|
|
1060
|
+
if not window_placement(platform_name).requests_position:
|
|
1061
|
+
return
|
|
1062
|
+
available = screen.availableGeometry().adjusted(SCREEN_MARGIN, SCREEN_MARGIN, -SCREEN_MARGIN, -SCREEN_MARGIN)
|
|
1063
|
+
decoration = unknown_decoration_estimate(window, decoration_reference, platform_name)
|
|
1064
|
+
frame = window.frameGeometry().marginsAdded(decoration)
|
|
1065
|
+
frame.moveCenter(centre)
|
|
1066
|
+
window.move(frame_position_inside(frame, available) + QPoint(decoration.left(), decoration.top()))
|