gitpane 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitpane/__init__.py +0 -0
- gitpane/account/__init__.py +0 -0
- gitpane/account/account_choice.py +297 -0
- gitpane/account/address_owner.py +67 -0
- gitpane/account/authentication.py +1811 -0
- gitpane/account/authentication_worker.py +836 -0
- gitpane/account/credential_manager.py +480 -0
- gitpane/account/credential_manager_install.py +171 -0
- gitpane/account/keyring_reader.py +135 -0
- gitpane/account/keyring_state.py +158 -0
- gitpane/account/ssh.py +312 -0
- gitpane/account/ssh_askpass.py +216 -0
- gitpane/account/ssh_prompt.py +222 -0
- gitpane/application.py +684 -0
- gitpane/commit/__init__.py +0 -0
- gitpane/commit/commit_metadata.py +88 -0
- gitpane/commit/commit_push.py +276 -0
- gitpane/commit/commit_write.py +830 -0
- gitpane/config.py +1066 -0
- gitpane/controller/__init__.py +0 -0
- gitpane/controller/account_controller.py +677 -0
- gitpane/controller/clone_controller.py +776 -0
- gitpane/controller/close_readiness.py +212 -0
- gitpane/controller/commit_timing_controller.py +566 -0
- gitpane/controller/conflict_controller.py +885 -0
- gitpane/controller/console_registry.py +38 -0
- gitpane/controller/emoji_window_controller.py +95 -0
- gitpane/controller/file_action_controller.py +479 -0
- gitpane/controller/history_read_controller.py +671 -0
- gitpane/controller/history_rewrite_controller.py +739 -0
- gitpane/controller/navigation_controller.py +658 -0
- gitpane/controller/opening_controller.py +375 -0
- gitpane/controller/operation_policy.py +76 -0
- gitpane/controller/rebase_controller.py +797 -0
- gitpane/controller/rebase_editor_controller.py +309 -0
- gitpane/controller/reference_form_controller.py +1677 -0
- gitpane/controller/reference_read_controller.py +242 -0
- gitpane/controller/refresh_controller.py +477 -0
- gitpane/controller/replay_controller.py +805 -0
- gitpane/controller/session_registry.py +337 -0
- gitpane/controller/setting_controller.py +592 -0
- gitpane/controller/shutdown_controller.py +345 -0
- gitpane/controller/staging_controller.py +199 -0
- gitpane/controller/staging_session_registry.py +47 -0
- gitpane/controller/stash_controller.py +367 -0
- gitpane/controller/transport_controller.py +813 -0
- gitpane/controller/welcome_controller.py +102 -0
- gitpane/controller/window_activation.py +116 -0
- gitpane/controller/worktree_state_controller.py +169 -0
- gitpane/emoji/__init__.py +0 -0
- gitpane/emoji/emoji_catalogue.py +156 -0
- gitpane/git_config/__init__.py +0 -0
- gitpane/git_config/git_setting.py +441 -0
- gitpane/history/__init__.py +0 -0
- gitpane/history/avatar.py +241 -0
- gitpane/history/file_history.py +359 -0
- gitpane/history/graph.py +145 -0
- gitpane/history/history_filter.py +31 -0
- gitpane/history/history_parse.py +605 -0
- gitpane/history/repository.py +1600 -0
- gitpane/history/repository_value.py +341 -0
- gitpane/history/revision.py +180 -0
- gitpane/history/revision_file.py +182 -0
- gitpane/history_rewrite/__init__.py +0 -0
- gitpane/history_rewrite/graph.py +210 -0
- gitpane/history_rewrite/guard.py +288 -0
- gitpane/history_rewrite/index.py +125 -0
- gitpane/history_rewrite/prepare.py +378 -0
- gitpane/history_rewrite/protocol.py +320 -0
- gitpane/history_rewrite/publish.py +444 -0
- gitpane/history_rewrite/recovery.py +481 -0
- gitpane/history_rewrite/value.py +229 -0
- gitpane/operation/__init__.py +0 -0
- gitpane/operation/console_line.py +313 -0
- gitpane/operation/console_tone.py +76 -0
- gitpane/operation/diagnostic.py +954 -0
- gitpane/operation/launch_signature.py +254 -0
- gitpane/operation/operation_status.py +87 -0
- gitpane/operation/operation_trace.py +766 -0
- gitpane/operation/repository_operation.py +781 -0
- gitpane/preference/__init__.py +0 -0
- gitpane/preference/preference_location.py +40 -0
- gitpane/preference/preference_store.py +590 -0
- gitpane/process/__init__.py +0 -0
- gitpane/process/exact_arguments.py +77 -0
- gitpane/process/git_output_text.py +20 -0
- gitpane/process/process_group.py +851 -0
- gitpane/process/process_log.py +114 -0
- gitpane/process/process_runner.py +1033 -0
- gitpane/process/released_child_watch.py +138 -0
- gitpane/recent/__init__.py +0 -0
- gitpane/recent/mount_table.py +422 -0
- gitpane/recent/recent_availability.py +414 -0
- gitpane/recent/recent_name.py +11 -0
- gitpane/recent/recent_search.py +114 -0
- gitpane/reference/__init__.py +0 -0
- gitpane/reference/reference_edit.py +2030 -0
- gitpane/reference/reference_snapshot.py +196 -0
- gitpane/reference/stash.py +730 -0
- gitpane/reference/stash_list.py +164 -0
- gitpane/sequencer/__init__.py +0 -0
- gitpane/sequencer/commit_message_rule.py +154 -0
- gitpane/sequencer/conflict.py +506 -0
- gitpane/sequencer/merge_recovery.py +163 -0
- gitpane/sequencer/merge_state.py +166 -0
- gitpane/sequencer/mergetool.py +856 -0
- gitpane/sequencer/rebase.py +750 -0
- gitpane/sequencer/rebase_edit_file.py +224 -0
- gitpane/sequencer/rebase_state.py +523 -0
- gitpane/sequencer/replay.py +1435 -0
- gitpane/sequencer/replay_legacy.py +253 -0
- gitpane/sequencer/replay_state.py +747 -0
- gitpane/submodule/__init__.py +0 -0
- gitpane/submodule/checkout_refusal.py +102 -0
- gitpane/submodule/checkout_refusal_notice.py +111 -0
- gitpane/submodule/cloned_without_checkout.py +65 -0
- gitpane/submodule/submodule_discovery.py +1180 -0
- gitpane/submodule/submodule_loss.py +218 -0
- gitpane/system/__init__.py +0 -0
- gitpane/system/appearance_portal.py +125 -0
- gitpane/system/application_identity.py +48 -0
- gitpane/system/command_line.py +36 -0
- gitpane/system/desktop_icon_fact.py +128 -0
- gitpane/system/desktop_icon_support.py +270 -0
- gitpane/system/desktop_shortcut.py +536 -0
- gitpane/system/icns_image.py +35 -0
- gitpane/system/installed_launcher.py +136 -0
- gitpane/system/linux_distribution.py +151 -0
- gitpane/system/network_folder.py +46 -0
- gitpane/system/path_identity.py +95 -0
- gitpane/system/private_tree_removal.py +47 -0
- gitpane/system/process_program.py +63 -0
- gitpane/system/qt_folder.py +38 -0
- gitpane/system/ready_channel_watch.py +67 -0
- gitpane/system/startup_activation.py +65 -0
- gitpane/system/terminal_detach.py +384 -0
- gitpane/system/xdg_session.py +85 -0
- gitpane/text/__init__.py +0 -0
- gitpane/text/text_query.py +97 -0
- gitpane/text/typed_path.py +75 -0
- gitpane/text/typed_text.py +17 -0
- gitpane/thread/__init__.py +0 -0
- gitpane/thread/weak_signal.py +93 -0
- gitpane/tool/__init__.py +0 -0
- gitpane/tool/app_bundle.py +164 -0
- gitpane/tool/desktop_text_editor.py +156 -0
- gitpane/tool/difftool.py +429 -0
- gitpane/tool/editor.py +445 -0
- gitpane/tool/editor_bridge.py +193 -0
- gitpane/tool/editor_recipe.py +481 -0
- gitpane/tool/file_reveal.py +94 -0
- gitpane/tool/git_executable.py +264 -0
- gitpane/tool/git_version.py +62 -0
- gitpane/tool/kdiff3.py +41 -0
- gitpane/tool/kdiff3_install.py +162 -0
- gitpane/tool/launch_services_text_editor.py +104 -0
- gitpane/tool/mergetool_recipe.py +84 -0
- gitpane/tool/system_editor_default.py +80 -0
- gitpane/tool/system_text_editor.py +44 -0
- gitpane/tool/system_tool.py +816 -0
- gitpane/tool/terminal_recipe.py +390 -0
- gitpane/transport/__init__.py +0 -0
- gitpane/transport/branch_exchange.py +1690 -0
- gitpane/transport/clone.py +1273 -0
- gitpane/transport/credential_read_retry.py +35 -0
- gitpane/transport/git_refusal.py +207 -0
- gitpane/transport/push_tracking.py +121 -0
- gitpane/transport/refusal_notice.py +225 -0
- gitpane/transport/remote.py +747 -0
- gitpane/transport/remote_config.py +544 -0
- gitpane/ui/__init__.py +0 -0
- gitpane/ui/console/__init__.py +0 -0
- gitpane/ui/console/error_dialog.py +332 -0
- gitpane/ui/console/operation_dialog.py +408 -0
- gitpane/ui/console/operation_output.py +91 -0
- gitpane/ui/console/operation_output_view.py +373 -0
- gitpane/ui/emoji/__init__.py +0 -0
- gitpane/ui/emoji/emoji_font.py +257 -0
- gitpane/ui/emoji/emoji_grid.py +125 -0
- gitpane/ui/emoji/emoji_window.py +154 -0
- gitpane/ui/emoji/file_action_icon.py +131 -0
- gitpane/ui/emoji/opentype_family_name.py +182 -0
- gitpane/ui/history_rewrite/__init__.py +0 -0
- gitpane/ui/history_rewrite/details.py +78 -0
- gitpane/ui/history_rewrite/preview.py +565 -0
- gitpane/ui/history_rewrite/preview_model.py +291 -0
- gitpane/ui/history_rewrite/question.py +87 -0
- gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
- gitpane/ui/look/__init__.py +0 -0
- gitpane/ui/look/action_icon.py +504 -0
- gitpane/ui/look/application_icon.py +78 -0
- gitpane/ui/look/application_style.py +317 -0
- gitpane/ui/look/avatar_label.py +53 -0
- gitpane/ui/look/color_contrast.py +43 -0
- gitpane/ui/look/desktop_appearance_follower.py +118 -0
- gitpane/ui/look/fixed_font.py +21 -0
- gitpane/ui/look/gtk_settings.py +116 -0
- gitpane/ui/look/icon_density.py +16 -0
- gitpane/ui/look/reference_font.py +19 -0
- gitpane/ui/look/theme_color.py +63 -0
- gitpane/ui/look/theme_look.py +95 -0
- gitpane/ui/look/theme_palette.py +173 -0
- gitpane/ui/look/warning_icon.py +86 -0
- gitpane/ui/look/windows_control_look.py +250 -0
- gitpane/ui/reader/__init__.py +0 -0
- gitpane/ui/reader/commit_message.py +129 -0
- gitpane/ui/reader/commit_panel.py +378 -0
- gitpane/ui/reader/diff_panel.py +626 -0
- gitpane/ui/reader/file_action.py +138 -0
- gitpane/ui/reader/file_tree_panel.py +235 -0
- gitpane/ui/reader/reader_toolbar.py +56 -0
- gitpane/ui/reader/revision_tree.py +80 -0
- gitpane/ui/reader/search_result.py +165 -0
- gitpane/ui/reader/source_text.py +134 -0
- gitpane/ui/reader/source_view.py +151 -0
- gitpane/ui/reader/syntax_span.py +67 -0
- gitpane/ui/reference/__init__.py +0 -0
- gitpane/ui/reference/branch_selector.py +106 -0
- gitpane/ui/reference/checkout_dialog.py +419 -0
- gitpane/ui/reference/checkout_revision_dialog.py +182 -0
- gitpane/ui/reference/commit_summary.py +339 -0
- gitpane/ui/reference/create_branch_dialog.py +346 -0
- gitpane/ui/reference/create_tag_dialog.py +283 -0
- gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
- gitpane/ui/reference/delete_tag_dialog.py +394 -0
- gitpane/ui/reference/form_opening.py +53 -0
- gitpane/ui/reference/reference_dialog.py +417 -0
- gitpane/ui/reference/reference_feedback.py +62 -0
- gitpane/ui/reference/reference_form.py +139 -0
- gitpane/ui/reference/rename_branch_dialog.py +179 -0
- gitpane/ui/reference/reset_branch_dialog.py +430 -0
- gitpane/ui/reference/reset_dialog.py +263 -0
- gitpane/ui/reference/revision_dialog.py +121 -0
- gitpane/ui/reference/revision_picker.py +227 -0
- gitpane/ui/reference/select_branches_dialog.py +199 -0
- gitpane/ui/reference/stash_dialog.py +171 -0
- gitpane/ui/repository/__init__.py +0 -0
- gitpane/ui/repository/graph_delegate.py +298 -0
- gitpane/ui/repository/history_avatar.py +130 -0
- gitpane/ui/repository/history_model.py +179 -0
- gitpane/ui/repository/repository_list_popup.py +319 -0
- gitpane/ui/repository/repository_window.py +2580 -0
- gitpane/ui/resource/ArrowDown.png +0 -0
- gitpane/ui/resource/ArrowUp.png +0 -0
- gitpane/ui/resource/BranchCheckout.png +0 -0
- gitpane/ui/resource/BranchCreate.png +0 -0
- gitpane/ui/resource/BranchDelete.png +0 -0
- gitpane/ui/resource/CopyToClipboard.png +0 -0
- gitpane/ui/resource/GitPane-16.png +0 -0
- gitpane/ui/resource/GitPane-24.png +0 -0
- gitpane/ui/resource/GitPane-256.png +0 -0
- gitpane/ui/resource/GitPane-32.png +0 -0
- gitpane/ui/resource/GitPane-48.png +0 -0
- gitpane/ui/resource/GitPane-64.png +0 -0
- gitpane/ui/resource/GitPane.ico +0 -0
- gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
- gitpane/ui/resource/Renamed.png +0 -0
- gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
- gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
- gitpane/ui/resource/Stash.png +0 -0
- gitpane/ui/resource/Tag.png +0 -0
- gitpane/ui/resource/TagCreate.png +0 -0
- gitpane/ui/resource/TagDelete.png +0 -0
- gitpane/ui/resource/Unmerged.png +0 -0
- gitpane/ui/resource/checkout.png +0 -0
- gitpane/ui/sequencer/__init__.py +0 -0
- gitpane/ui/sequencer/conflict_dialog.py +532 -0
- gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
- gitpane/ui/sequencer/rebase_dialog.py +461 -0
- gitpane/ui/sequencer/rebase_editor_window.py +220 -0
- gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
- gitpane/ui/sequencer/replay_dialog.py +501 -0
- gitpane/ui/setting/__init__.py +0 -0
- gitpane/ui/setting/account_page.py +724 -0
- gitpane/ui/setting/add_account_dialog.py +1005 -0
- gitpane/ui/setting/credential_manager_dialog.py +151 -0
- gitpane/ui/setting/git_setting_widget.py +388 -0
- gitpane/ui/setting/setting_block.py +19 -0
- gitpane/ui/setting/setting_dialog.py +750 -0
- gitpane/ui/setting/ssh_widget.py +232 -0
- gitpane/ui/setting/utility_page.py +240 -0
- gitpane/ui/staging/__init__.py +0 -0
- gitpane/ui/staging/staging_action.py +356 -0
- gitpane/ui/staging/staging_window.py +745 -0
- gitpane/ui/transport/__init__.py +0 -0
- gitpane/ui/transport/clone_dialog.py +879 -0
- gitpane/ui/transport/push_dialog.py +580 -0
- gitpane/ui/transport/push_rejection_prompt.py +100 -0
- gitpane/ui/transport/push_tracking_question.py +88 -0
- gitpane/ui/transport/remote_dialog.py +943 -0
- gitpane/ui/welcome/__init__.py +0 -0
- gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
- gitpane/ui/welcome/welcome_window.py +1137 -0
- gitpane/ui/widget/__init__.py +0 -0
- gitpane/ui/widget/busy_indicator.py +106 -0
- gitpane/ui/widget/commit_date_text.py +55 -0
- gitpane/ui/widget/content_growth.py +262 -0
- gitpane/ui/widget/content_measure.py +99 -0
- gitpane/ui/widget/dialog_layout.py +668 -0
- gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
- gitpane/ui/widget/flow_layout.py +110 -0
- gitpane/ui/widget/long_text.py +542 -0
- gitpane/ui/widget/mnemonic.py +13 -0
- gitpane/ui/widget/numbered_text_edit.py +127 -0
- gitpane/ui/widget/plain_label.py +15 -0
- gitpane/ui/widget/plain_tooltip.py +19 -0
- gitpane/ui/widget/refresh_advice.py +25 -0
- gitpane/ui/widget/result_label.py +227 -0
- gitpane/ui/widget/result_status_bar.py +53 -0
- gitpane/ui/widget/scrolled_text.py +69 -0
- gitpane/ui/widget/shortcut_text.py +31 -0
- gitpane/ui/widget/standard_icon_label.py +55 -0
- gitpane/ui/widget/system_shortcut.py +75 -0
- gitpane/ui/widget/unavailable_reason.py +29 -0
- gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
- gitpane/ui/window_control/__init__.py +0 -0
- gitpane/ui/window_control/button_focus.py +42 -0
- gitpane/ui/window_control/compositor_answer.py +152 -0
- gitpane/ui/window_control/compositor_growth_cap.py +73 -0
- gitpane/ui/window_control/desktop_activation.py +203 -0
- gitpane/ui/window_control/dialog_modality.py +122 -0
- gitpane/ui/window_control/item_activation.py +42 -0
- gitpane/ui/window_control/modal_focus_relay.py +124 -0
- gitpane/ui/window_control/wayland_activation.py +695 -0
- gitpane/ui/window_control/window_placement.py +305 -0
- gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- gitpane/vendor/w32.exe +0 -0
- gitpane/vendor/w64-arm.exe +0 -0
- gitpane/vendor/w64.exe +0 -0
- gitpane/worktree/__init__.py +0 -0
- gitpane/worktree/command_sequence.py +110 -0
- gitpane/worktree/file_action_target.py +61 -0
- gitpane/worktree/file_menu_action.py +52 -0
- gitpane/worktree/historical_restore.py +361 -0
- gitpane/worktree/ignore_rule.py +266 -0
- gitpane/worktree/mutation.py +1539 -0
- gitpane/worktree/partial_patch.py +435 -0
- gitpane/worktree/staged_reset.py +447 -0
- gitpane/worktree/staging.py +1418 -0
- gitpane/worktree/trash.py +302 -0
- gitpane/worktree/worktree_file.py +406 -0
- gitpane/worktree/worktree_service.py +584 -0
- gitpane/worktree/worktree_snapshot.py +630 -0
- gitpane-1.0.0.dist-info/METADATA +141 -0
- gitpane-1.0.0.dist-info/RECORD +353 -0
- gitpane-1.0.0.dist-info/WHEEL +5 -0
- gitpane-1.0.0.dist-info/entry_points.txt +2 -0
- gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
- gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- gitpane-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""Give Settings targeted access to Git configuration values, without copying whole config files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import TYPE_CHECKING, Self
|
|
11
|
+
|
|
12
|
+
from PySide6.QtCore import QObject, QTimer, Signal
|
|
13
|
+
|
|
14
|
+
from gitpane.config import GIT_MUTATION_TIMEOUT_MS
|
|
15
|
+
from gitpane.history.repository_value import RepositoryContext
|
|
16
|
+
from gitpane.operation.repository_operation import OWNED_REPOSITORY_TEXT, OperationAction, OperationIntent, StaleCaptureError
|
|
17
|
+
from gitpane.process.process_runner import LOCAL_WRITE_PROFILE, READ_PROFILE
|
|
18
|
+
from gitpane.transport.remote_config import GitConfigScope, config_scope, parse_config
|
|
19
|
+
from gitpane.worktree.command_sequence import GitCommand, GitCommandError, ReadBatch
|
|
20
|
+
from gitpane.worktree.worktree_snapshot import fingerprint
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Callable, Generator
|
|
24
|
+
|
|
25
|
+
from gitpane.process.process_runner import ProcessRunner
|
|
26
|
+
from gitpane.transport.remote_config import RemoteValue
|
|
27
|
+
from gitpane.worktree.mutation import MutationService
|
|
28
|
+
|
|
29
|
+
AUTOCRLF_KEY = "core.autocrlf"
|
|
30
|
+
GIT_SETTING_KEYS = ("user.name", "user.email", AUTOCRLF_KEY)
|
|
31
|
+
SETTING_LABEL_BY_KEY = {"user.name": "Name", "user.email": "Email", AUTOCRLF_KEY: "Line endings"}
|
|
32
|
+
# The scopes Settings writes, in the order it shows them.
|
|
33
|
+
SETTING_SCOPES = (GitConfigScope.GLOBAL, GitConfigScope.LOCAL)
|
|
34
|
+
AUTOCRLF_DESCRIPTION_BY_VALUE = {
|
|
35
|
+
"true": "Check out Windows-style, commit Unix-style line endings",
|
|
36
|
+
"input": "Check out as-is, commit Unix-style line endings",
|
|
37
|
+
"false": "No line-ending conversion by core.autocrlf",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SettingView(Enum):
|
|
42
|
+
"""One reading of a setting that Settings keeps for each key, with the scope whose values it keeps.
|
|
43
|
+
|
|
44
|
+
The global and local views keep, from the effective listing, only the values Git attributes to their scope; the
|
|
45
|
+
direct view is the repository's own file without its includes, and the effective view is the value Git uses.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
GLOBAL = ("global", GitConfigScope.GLOBAL)
|
|
49
|
+
LOCAL = ("local", GitConfigScope.LOCAL)
|
|
50
|
+
DIRECT = ("direct", None)
|
|
51
|
+
EFFECTIVE = ("effective", None)
|
|
52
|
+
|
|
53
|
+
def __new__(cls, value: str, scope: GitConfigScope | None) -> Self:
|
|
54
|
+
"""Keep the view's name as the value and the scope it keeps beside it."""
|
|
55
|
+
member = object.__new__(cls)
|
|
56
|
+
member._value_ = value
|
|
57
|
+
member.scope = scope
|
|
58
|
+
return member
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def for_scope(cls, scope: GitConfigScope) -> SettingView:
|
|
62
|
+
"""Answer the view that shows the values of one scope Settings writes."""
|
|
63
|
+
for view in cls:
|
|
64
|
+
if view.scope == scope:
|
|
65
|
+
return view
|
|
66
|
+
raise ValueError("No Settings view keeps this Git configuration scope")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GitSettingOperation(Enum):
|
|
70
|
+
"""What one Settings change does to a key: write the value, or remove every value of the key in its scope."""
|
|
71
|
+
|
|
72
|
+
SET = "set"
|
|
73
|
+
UNSET = "unset"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
REPOSITORY_SETTING_VIEWS = (SettingView.GLOBAL, SettingView.LOCAL, SettingView.DIRECT, SettingView.EFFECTIVE)
|
|
77
|
+
GLOBAL_SETTING_VIEWS = (SettingView.GLOBAL, SettingView.EFFECTIVE)
|
|
78
|
+
SETTING_READ_ACCEPTED_CODES = (0, 1)
|
|
79
|
+
SETTING_FIELD_TERMINATOR = b"\0"
|
|
80
|
+
SETTING_READ_OPTIONS = ("config", "--null", "--show-origin", "--show-scope")
|
|
81
|
+
# The grouped reads search the supported keys only: no unrelated value, secrets included, is ever read, and a missing
|
|
82
|
+
# global file answers "nothing found" instead of failing the way a listing of every global value does.
|
|
83
|
+
SETTING_KEY_PATTERN = "^(" + "|".join(re.escape(key) for key in GIT_SETTING_KEYS) + ")$"
|
|
84
|
+
AUTOCRLF_KEY_PATTERN = "^" + re.escape(AUTOCRLF_KEY) + "$"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class GitSettingValue:
|
|
89
|
+
"""Distinguish absence, explicit empty values and unreadable configuration."""
|
|
90
|
+
|
|
91
|
+
value: str | None = None
|
|
92
|
+
scope: GitConfigScope | None = None
|
|
93
|
+
origin: str = ""
|
|
94
|
+
state: str = "absent"
|
|
95
|
+
source_fingerprint: str = "absent"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class GitSettingSnapshot:
|
|
100
|
+
"""Capture direct and resolved values in the repository chosen when settings opened."""
|
|
101
|
+
|
|
102
|
+
context: RepositoryContext | None
|
|
103
|
+
git_filename: str
|
|
104
|
+
generation: int
|
|
105
|
+
value_by_target: dict[tuple[SettingView, str], GitSettingValue]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class GitSettingChange:
|
|
110
|
+
"""Carry one exact explicit edit independently of other save outcomes."""
|
|
111
|
+
|
|
112
|
+
key: str
|
|
113
|
+
scope: GitConfigScope
|
|
114
|
+
operation: GitSettingOperation
|
|
115
|
+
value: str = ""
|
|
116
|
+
|
|
117
|
+
def validate(self, context: RepositoryContext | None) -> None:
|
|
118
|
+
"""Reject unsupported keys, scopes and operations before any process or write starts.
|
|
119
|
+
|
|
120
|
+
Only a member names an operation: the write treats anything that is not SET as a removal, so the text of an
|
|
121
|
+
operation, or any other value, would erase the user's setting instead of being refused.
|
|
122
|
+
"""
|
|
123
|
+
if self.key not in GIT_SETTING_KEYS or self.scope not in SETTING_SCOPES or not isinstance(self.operation, GitSettingOperation):
|
|
124
|
+
raise ValueError("Unsupported Git setting change")
|
|
125
|
+
if self.scope == GitConfigScope.LOCAL and context is None:
|
|
126
|
+
raise ValueError("No repository was selected for these settings")
|
|
127
|
+
if "\0" in self.value:
|
|
128
|
+
raise ValueError("Git setting values cannot contain NUL characters")
|
|
129
|
+
if self.key == AUTOCRLF_KEY and self.operation == GitSettingOperation.SET and self.value not in AUTOCRLF_DESCRIPTION_BY_VALUE:
|
|
130
|
+
raise ValueError("Choose true, input or false for core.autocrlf")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class GitSettingResult:
|
|
135
|
+
"""Retain one write result even if another key could not be saved."""
|
|
136
|
+
|
|
137
|
+
change: GitSettingChange
|
|
138
|
+
saved: bool
|
|
139
|
+
cause: str = ""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def parse_value(output: bytes) -> GitSettingValue:
|
|
143
|
+
"""Decode Git's NUL scope/origin/value triples, retaining the last effective entry."""
|
|
144
|
+
if not output:
|
|
145
|
+
return GitSettingValue()
|
|
146
|
+
if not output.endswith(b"\0"):
|
|
147
|
+
raise ValueError("Incomplete Git setting output")
|
|
148
|
+
fields = iter(output.removesuffix(b"\0").split(b"\0"))
|
|
149
|
+
result = GitSettingValue()
|
|
150
|
+
try:
|
|
151
|
+
for scope in fields:
|
|
152
|
+
origin, value = next(fields), next(fields)
|
|
153
|
+
text = value.decode("utf-8", "replace")
|
|
154
|
+
result = GitSettingValue(text, config_scope(scope.decode("utf-8", "replace")), origin.decode("utf-8", "replace"), "empty" if not text else "present", fingerprint(output))
|
|
155
|
+
except StopIteration as error:
|
|
156
|
+
raise ValueError("Incomplete Git setting record") from error
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def classified_setting_value(key: str, value: GitSettingValue) -> GitSettingValue:
|
|
161
|
+
"""Mark a core.autocrlf value the form cannot offer as invalid, keeping what Git answered."""
|
|
162
|
+
if key == AUTOCRLF_KEY and value.value is not None and value.value not in AUTOCRLF_DESCRIPTION_BY_VALUE:
|
|
163
|
+
return GitSettingValue(value.value, value.scope, value.origin, "invalid", value.source_fingerprint)
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def setting_value_from_records(values: tuple[RemoteValue, ...], key: str) -> GitSettingValue:
|
|
168
|
+
"""Read one key from listed config records exactly as from Git's own answer for that key alone.
|
|
169
|
+
|
|
170
|
+
Only the key's occurrences are kept and written back in the scope, origin and value triples Git prints for that
|
|
171
|
+
key, so the final value, its origin, an absent or empty value and the fingerprint of the occurrences all come
|
|
172
|
+
from parse_value; other settings of the listing never enter the fingerprint.
|
|
173
|
+
"""
|
|
174
|
+
records: list[bytes] = []
|
|
175
|
+
for value in values:
|
|
176
|
+
if value.key != key:
|
|
177
|
+
continue
|
|
178
|
+
for text in (value.scope.value, value.origin, value.value):
|
|
179
|
+
records.append(text.encode("utf-8", "surrogateescape") + SETTING_FIELD_TERMINATOR)
|
|
180
|
+
return classified_setting_value(key, parse_value(b"".join(records)))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def count_by_source(values: tuple[RemoteValue, ...]) -> dict[tuple[GitConfigScope, str], int]:
|
|
184
|
+
"""Count occurrences per scope and origin, the identity Git gives each configuration source."""
|
|
185
|
+
result: dict[tuple[GitConfigScope, str], int] = {}
|
|
186
|
+
for value in values:
|
|
187
|
+
source = (value.scope, value.origin)
|
|
188
|
+
result[source] = result.get(source, 0) + 1
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def converted_occurrences(selected_values: tuple[RemoteValue, ...], raw_values: tuple[RemoteValue, ...],
|
|
193
|
+
typed_values: tuple[RemoteValue, ...]) -> tuple[RemoteValue, ...] | None:
|
|
194
|
+
"""Replace a view's raw occurrences of a key by the ones Git converted, matched by scope, origin and position.
|
|
195
|
+
|
|
196
|
+
``raw_values`` and ``typed_values`` list the same effective occurrences from two reads; the selected occurrences
|
|
197
|
+
come from either the effective listing or the listing of the repository's own file, and always hold every
|
|
198
|
+
occurrence of their sources, so the k-th selected occurrence of a source is the k-th converted one of that source.
|
|
199
|
+
Answers None when the reads or the view do not pair one to one, so a converted value never lands on the wrong
|
|
200
|
+
occurrence.
|
|
201
|
+
"""
|
|
202
|
+
typed_by_source: dict[tuple[GitConfigScope, str], list[RemoteValue]] = {}
|
|
203
|
+
for value in typed_values:
|
|
204
|
+
typed_by_source.setdefault((value.scope, value.origin), []).append(value)
|
|
205
|
+
typed_count_by_source: dict[tuple[GitConfigScope, str], int] = {}
|
|
206
|
+
for source, typed in typed_by_source.items():
|
|
207
|
+
typed_count_by_source[source] = len(typed)
|
|
208
|
+
if count_by_source(raw_values) != typed_count_by_source:
|
|
209
|
+
return None
|
|
210
|
+
for source, count in count_by_source(selected_values).items():
|
|
211
|
+
if typed_count_by_source.get(source, 0) != count:
|
|
212
|
+
return None
|
|
213
|
+
position_by_source: dict[tuple[GitConfigScope, str], int] = {}
|
|
214
|
+
converted: list[RemoteValue] = []
|
|
215
|
+
for value in selected_values:
|
|
216
|
+
source = (value.scope, value.origin)
|
|
217
|
+
position = position_by_source.get(source, 0)
|
|
218
|
+
converted.append(typed_by_source[source][position])
|
|
219
|
+
position_by_source[source] = position + 1
|
|
220
|
+
return tuple(converted)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def key_values(values: tuple[RemoteValue, ...], key: str) -> tuple[RemoteValue, ...]:
|
|
224
|
+
"""Keep the occurrences of one key, in their listed order."""
|
|
225
|
+
selected: list[RemoteValue] = []
|
|
226
|
+
for value in values:
|
|
227
|
+
if value.key == key:
|
|
228
|
+
selected.append(value)
|
|
229
|
+
return tuple(selected)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class GitSettingService(QObject):
|
|
233
|
+
"""Serialize application config writes while letting Git own its native file locks."""
|
|
234
|
+
|
|
235
|
+
changed = Signal(object)
|
|
236
|
+
|
|
237
|
+
def __init__(self, runner: ProcessRunner, mutation_service: MutationService, parent: QObject | None = None) -> None:
|
|
238
|
+
"""Share bounded asynchronous command execution and mutation ownership."""
|
|
239
|
+
super().__init__(parent)
|
|
240
|
+
self.runner = runner
|
|
241
|
+
self.mutation_service = mutation_service
|
|
242
|
+
self.worktree_service = mutation_service.worktree_service
|
|
243
|
+
self.generation = 0
|
|
244
|
+
|
|
245
|
+
def read(self, context: RepositoryContext | None, git_filename: str, callback: Callable, is_current: Callable[[], bool] | None = None) -> Generator:
|
|
246
|
+
"""Read only supported keys, never writing defaults or enabling worktree configuration."""
|
|
247
|
+
sequence = self.capture(context, git_filename, is_current)
|
|
248
|
+
self.worktree_service.run(self._command_context(context), git_filename, sequence, callback)
|
|
249
|
+
return sequence
|
|
250
|
+
|
|
251
|
+
def cancel_read(self, sequence: Generator) -> None:
|
|
252
|
+
"""Cancel only this form's read, leaving shared worktree requests and saves alone."""
|
|
253
|
+
for request_id, continuation in tuple(self.worktree_service.continuation_by_id.items()):
|
|
254
|
+
current_sequence, *_rest = continuation
|
|
255
|
+
if current_sequence is sequence:
|
|
256
|
+
self.runner.cancel(request_id)
|
|
257
|
+
|
|
258
|
+
def _command_context(self, context: RepositoryContext | None) -> RepositoryContext:
|
|
259
|
+
"""Use the user's home for global-only reads so a checkout cannot affect includes."""
|
|
260
|
+
home_dir = os.path.expanduser("~")
|
|
261
|
+
return context if context is not None else RepositoryContext(None, home_dir, home_dir, True)
|
|
262
|
+
|
|
263
|
+
def capture(self, context: RepositoryContext | None, git_filename: str, is_current: Callable[[], bool] | None = None) -> Generator:
|
|
264
|
+
"""Read the global, local, direct and effective views of every supported key with three Git reads.
|
|
265
|
+
|
|
266
|
+
One effective search of the supported keys gives the global, local and effective views, one search of the
|
|
267
|
+
repository's own file gives the direct view, and Git itself converts core.autocrlf with bool-or-str; outside a
|
|
268
|
+
repository the two global reads suffice. Whatever these reads cannot prove complete, a failed or cut listing, or converted values
|
|
269
|
+
that cannot be paired with their occurrences, is read again one key and view at a time, so an error stays
|
|
270
|
+
attached to the value it concerns. Nothing read here is kept for another operation.
|
|
271
|
+
"""
|
|
272
|
+
if is_current is not None and not is_current():
|
|
273
|
+
return None
|
|
274
|
+
in_repository = context is not None
|
|
275
|
+
# Outside a repository only the global files count, and the commands run from the home directory.
|
|
276
|
+
global_options = () if in_repository else ("--global",)
|
|
277
|
+
commands = [GitCommand([*SETTING_READ_OPTIONS, "--includes", *global_options, "--get-regexp", SETTING_KEY_PATTERN], accepted_codes=SETTING_READ_ACCEPTED_CODES, profile=READ_PROFILE),
|
|
278
|
+
GitCommand([*SETTING_READ_OPTIONS, "--includes", *global_options, "--type=bool-or-str", "--get-regexp", AUTOCRLF_KEY_PATTERN],
|
|
279
|
+
accepted_codes=SETTING_READ_ACCEPTED_CODES, profile=READ_PROFILE)]
|
|
280
|
+
if in_repository:
|
|
281
|
+
commands.append(GitCommand([*SETTING_READ_OPTIONS, "--local", "--no-includes", "--get-regexp", SETTING_KEY_PATTERN], accepted_codes=SETTING_READ_ACCEPTED_CODES, profile=READ_PROFILE))
|
|
282
|
+
try:
|
|
283
|
+
effective_output, typed_output, *direct_outputs = yield ReadBatch(tuple(commands))
|
|
284
|
+
effective_values = parse_config(effective_output)
|
|
285
|
+
typed_values = parse_config(typed_output)
|
|
286
|
+
direct_values: tuple[RemoteValue, ...] = ()
|
|
287
|
+
for direct_output in direct_outputs:
|
|
288
|
+
direct_values = parse_config(direct_output)
|
|
289
|
+
except (GitCommandError, ValueError):
|
|
290
|
+
return (yield from self._capture_separately(context, git_filename, is_current))
|
|
291
|
+
if is_current is not None and not is_current():
|
|
292
|
+
return None
|
|
293
|
+
value_by_view: dict[SettingView, tuple[RemoteValue, ...]] = {SettingView.EFFECTIVE: effective_values}
|
|
294
|
+
for view in SettingView:
|
|
295
|
+
if view.scope is None:
|
|
296
|
+
continue
|
|
297
|
+
scoped_values: list[RemoteValue] = []
|
|
298
|
+
for value in effective_values:
|
|
299
|
+
if value.scope == view.scope:
|
|
300
|
+
scoped_values.append(value)
|
|
301
|
+
value_by_view[view] = tuple(scoped_values)
|
|
302
|
+
if in_repository:
|
|
303
|
+
value_by_view[SettingView.DIRECT] = direct_values
|
|
304
|
+
value_by_target: dict[tuple[SettingView, str], GitSettingValue] = {}
|
|
305
|
+
for key in GIT_SETTING_KEYS:
|
|
306
|
+
for view in REPOSITORY_SETTING_VIEWS if in_repository else GLOBAL_SETTING_VIEWS:
|
|
307
|
+
view_values = key_values(value_by_view[view], key)
|
|
308
|
+
if key == AUTOCRLF_KEY:
|
|
309
|
+
converted = converted_occurrences(view_values, key_values(effective_values, key), key_values(typed_values, key))
|
|
310
|
+
if converted is None:
|
|
311
|
+
# Occurrences Git converted in another read cannot be paired: ask Git for this view alone.
|
|
312
|
+
value_by_target[(view, key)] = yield from self._read_view(context, key, view)
|
|
313
|
+
continue
|
|
314
|
+
view_values = converted
|
|
315
|
+
value_by_target[(view, key)] = setting_value_from_records(view_values, key)
|
|
316
|
+
self.generation += 1
|
|
317
|
+
return GitSettingSnapshot(context, git_filename, self.generation, value_by_target)
|
|
318
|
+
|
|
319
|
+
def _capture_separately(self, context: RepositoryContext | None, git_filename: str, is_current: Callable[[], bool] | None = None) -> Generator:
|
|
320
|
+
"""Read each key in each view with its own Git command, so one unreadable source fails only the values it affects."""
|
|
321
|
+
value_by_target: dict[tuple[SettingView, str], GitSettingValue] = {}
|
|
322
|
+
for key in GIT_SETTING_KEYS:
|
|
323
|
+
for view in REPOSITORY_SETTING_VIEWS if context is not None else GLOBAL_SETTING_VIEWS:
|
|
324
|
+
if is_current is not None and not is_current():
|
|
325
|
+
return None
|
|
326
|
+
value_by_target[(view, key)] = yield from self._read_view(context, key, view)
|
|
327
|
+
self.generation += 1
|
|
328
|
+
return GitSettingSnapshot(context, git_filename, self.generation, value_by_target)
|
|
329
|
+
|
|
330
|
+
def _read_view(self, context: RepositoryContext | None, key: str, view: SettingView) -> Generator:
|
|
331
|
+
"""Read one key in one view with one Git command, answering an error state rather than raising."""
|
|
332
|
+
arguments = [*SETTING_READ_OPTIONS, "--get-all"]
|
|
333
|
+
if view.scope is not None:
|
|
334
|
+
arguments.extend(["--" + view.scope.value, "--includes"])
|
|
335
|
+
elif view == SettingView.DIRECT:
|
|
336
|
+
arguments.extend(["--local", "--no-includes"])
|
|
337
|
+
else:
|
|
338
|
+
arguments.append("--includes")
|
|
339
|
+
if context is None:
|
|
340
|
+
arguments.append("--global")
|
|
341
|
+
arguments.append(key)
|
|
342
|
+
if key == AUTOCRLF_KEY:
|
|
343
|
+
arguments.insert(len(arguments) - 1, "--type=bool-or-str")
|
|
344
|
+
try:
|
|
345
|
+
output = yield GitCommand(arguments, accepted_codes=SETTING_READ_ACCEPTED_CODES, profile=READ_PROFILE)
|
|
346
|
+
return classified_setting_value(key, parse_value(output))
|
|
347
|
+
except (GitCommandError, ValueError):
|
|
348
|
+
# Git stderr may contain arbitrary included configuration; expose only the key's failure.
|
|
349
|
+
return GitSettingValue(state="error")
|
|
350
|
+
|
|
351
|
+
def save(self, snapshot: GitSettingSnapshot, changes: tuple[GitSettingChange, ...], callback: Callable) -> None:
|
|
352
|
+
"""Reserve shared configuration and report successful writes without rolling them back.
|
|
353
|
+
|
|
354
|
+
When nothing in the availability checks explains a refusal, a write really holds the worktree or the global
|
|
355
|
+
configuration (another settings save, an index change), so the refusal names that running operation.
|
|
356
|
+
"""
|
|
357
|
+
for change in changes:
|
|
358
|
+
change.validate(snapshot.context)
|
|
359
|
+
operation_id = uuid.uuid4().hex
|
|
360
|
+
global_configuration = any(change.scope == GitConfigScope.GLOBAL for change in changes)
|
|
361
|
+
context = self._command_context(snapshot.context)
|
|
362
|
+
intent = OperationIntent.capture(operation_id, context, snapshot.git_filename, generation=snapshot.generation,
|
|
363
|
+
global_configuration=global_configuration, action=OperationAction.CONFIG_WRITE)
|
|
364
|
+
mutation = self.mutation_service
|
|
365
|
+
legacy_busy = bool(mutation.active_key_set) if global_configuration else intent.key in mutation.active_key_set
|
|
366
|
+
reason = mutation.repository_operation.availability_diagnostic(intent)
|
|
367
|
+
if mutation.configuration_busy or legacy_busy or not mutation.repository_operation.reserve(intent):
|
|
368
|
+
results = tuple(GitSettingResult(change, False, reason or OWNED_REPOSITORY_TEXT) for change in changes)
|
|
369
|
+
QTimer.singleShot(0, lambda: callback((snapshot, results), None))
|
|
370
|
+
return
|
|
371
|
+
if global_configuration:
|
|
372
|
+
mutation.configuration_busy = True
|
|
373
|
+
|
|
374
|
+
def complete(result: object, error: Exception | None) -> None:
|
|
375
|
+
"""Release ownership after all commands finish, including failed or closed views."""
|
|
376
|
+
if global_configuration:
|
|
377
|
+
mutation.configuration_busy = False
|
|
378
|
+
mutation.repository_operation.release(operation_id)
|
|
379
|
+
callback(result, error)
|
|
380
|
+
|
|
381
|
+
self.worktree_service.run(context, snapshot.git_filename, self._save(snapshot, changes, intent), complete, operation_id=operation_id)
|
|
382
|
+
|
|
383
|
+
def _save(self, snapshot: GitSettingSnapshot, changes: tuple[GitSettingChange, ...], intent: OperationIntent) -> Generator:
|
|
384
|
+
"""Revalidate targeted values before each write; cross-process writes remain nontransactional.
|
|
385
|
+
|
|
386
|
+
Each change reads the configuration again just before its own write, and again after it to report the result.
|
|
387
|
+
The last of those reads is what the form shows: it is already fresh, so no further read follows it; a save
|
|
388
|
+
with no change still reads the configuration once. A write refused by the check just before Git starts, for
|
|
389
|
+
example because GitPane began closing between two changes, gives that check's reason rather than blaming file
|
|
390
|
+
permissions.
|
|
391
|
+
"""
|
|
392
|
+
expected_by_target = dict(snapshot.value_by_target)
|
|
393
|
+
results: list[GitSettingResult] = []
|
|
394
|
+
latest = None
|
|
395
|
+
launch_refusal_causes: list[str] = []
|
|
396
|
+
|
|
397
|
+
def validate_before_write() -> None:
|
|
398
|
+
"""Recheck the reservation just before Git starts, keeping the reason of a refusal so the result can give it."""
|
|
399
|
+
try:
|
|
400
|
+
self.mutation_service.repository_operation.validate(intent)
|
|
401
|
+
except StaleCaptureError as error:
|
|
402
|
+
launch_refusal_causes.append(str(error))
|
|
403
|
+
raise
|
|
404
|
+
|
|
405
|
+
for change in changes:
|
|
406
|
+
current = yield from self.capture(snapshot.context, snapshot.git_filename)
|
|
407
|
+
latest = current
|
|
408
|
+
changed = False
|
|
409
|
+
unreadable = False
|
|
410
|
+
for target, expected in expected_by_target.items():
|
|
411
|
+
_view, key = target
|
|
412
|
+
if key != change.key:
|
|
413
|
+
continue
|
|
414
|
+
actual = current.value_by_target[target]
|
|
415
|
+
changed |= actual != expected
|
|
416
|
+
unreadable |= actual.state == "error" or expected.state == "error"
|
|
417
|
+
if changed or unreadable:
|
|
418
|
+
results.append(GitSettingResult(change, False, "Configuration changed or could not be read. Review the refreshed values and try Save again."))
|
|
419
|
+
continue
|
|
420
|
+
arguments = ["config", "--" + change.scope.value, "--replace-all" if change.operation == GitSettingOperation.SET else "--unset-all", change.key]
|
|
421
|
+
if change.operation == GitSettingOperation.SET:
|
|
422
|
+
arguments.append(change.value)
|
|
423
|
+
launch_refusal_causes.clear()
|
|
424
|
+
try:
|
|
425
|
+
yield GitCommand(arguments, accepted_codes=(0,) if change.operation == GitSettingOperation.SET else (0, 5), timeout_ms=GIT_MUTATION_TIMEOUT_MS,
|
|
426
|
+
before_start=validate_before_write, profile=LOCAL_WRITE_PROFILE)
|
|
427
|
+
results.append(GitSettingResult(change, True))
|
|
428
|
+
except (GitCommandError, ValueError):
|
|
429
|
+
cause = next(iter(launch_refusal_causes), "Git could not write this setting. Check file permissions or an existing config lock, then retry.")
|
|
430
|
+
results.append(GitSettingResult(change, False, cause))
|
|
431
|
+
refreshed = yield from self.capture(snapshot.context, snapshot.git_filename)
|
|
432
|
+
latest = refreshed
|
|
433
|
+
for target, value in refreshed.value_by_target.items():
|
|
434
|
+
_view, key = target
|
|
435
|
+
if key == change.key:
|
|
436
|
+
expected_by_target[target] = value
|
|
437
|
+
if latest is None:
|
|
438
|
+
latest = yield from self.capture(snapshot.context, snapshot.git_filename)
|
|
439
|
+
if any(result.saved for result in results):
|
|
440
|
+
self.changed.emit((snapshot.context, tuple(results)))
|
|
441
|
+
return latest, tuple(results)
|
|
File without changes
|