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,544 @@
|
|
|
1
|
+
"""Read the exact remote configuration of a repository from Git's config output.
|
|
2
|
+
|
|
3
|
+
Each value keeps the scope and the file Git read it from, and the remotes, the branch tracking settings and the rewrite
|
|
4
|
+
rules are interpreted the way Git itself interprets them, so the edits and the transfers work on what Git will use.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from gitpane.config import LOCAL_PREFIX, REFERENCE_ROOT_PREFIX, REMOTE_PREFIX, TAG_PREFIX
|
|
16
|
+
from gitpane.reference.reference_snapshot import REFERENCE_LINE_TERMINATOR, parse_reference_read
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from gitpane.history.repository_value import RepositoryContext
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GitConfigScope(Enum):
|
|
23
|
+
"""The source of a Git setting as Git names it, in what GitPane reads back as in what it writes.
|
|
24
|
+
|
|
25
|
+
The value is Git's word: the one ``git config --show-scope`` prints before each value, and the one GitPane puts
|
|
26
|
+
after "--" to choose the file a write goes to. The first members are the scopes Git's documentation of --show-scope
|
|
27
|
+
lists, in its order; ``UNKNOWN`` stands for any other word Git prints, and is never a scope GitPane writes to.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
WORKTREE = "worktree"
|
|
31
|
+
LOCAL = "local"
|
|
32
|
+
GLOBAL = "global"
|
|
33
|
+
SYSTEM = "system"
|
|
34
|
+
COMMAND = "command"
|
|
35
|
+
UNKNOWN = "unknown"
|
|
36
|
+
"""A scope word outside those the documentation of --show-scope lists, such as ``submodule``; ``unknown`` is Git's own
|
|
37
|
+
word for a source it cannot name. A setting is never written to it: the settings window only writes the global and
|
|
38
|
+
the local scopes."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def config_scope(text: str) -> GitConfigScope:
|
|
42
|
+
"""Read the scope word Git printed before a configuration value.
|
|
43
|
+
|
|
44
|
+
A word Git does not document reads as ``UNKNOWN`` instead of failing the whole read; the word as printed stays in
|
|
45
|
+
``ConfigRecord.scope_text``, which the console trace shows.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
return GitConfigScope(text)
|
|
49
|
+
except ValueError:
|
|
50
|
+
return GitConfigScope.UNKNOWN
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class RemoteValue:
|
|
55
|
+
"""Keep every config value with its native scope and source filename."""
|
|
56
|
+
|
|
57
|
+
key: str
|
|
58
|
+
value: str
|
|
59
|
+
scope: GitConfigScope
|
|
60
|
+
origin: str
|
|
61
|
+
has_value: bool = True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class ConfigRecord:
|
|
66
|
+
"""One record of a NUL-delimited configuration listing, as the texts Git printed.
|
|
67
|
+
|
|
68
|
+
``scope_text`` is the scope word as printed, before any reading of it; ``has_value`` tells a key Git listed with a
|
|
69
|
+
value, even an empty one, from a key it listed without any.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
scope_text: str
|
|
73
|
+
origin: str
|
|
74
|
+
key: str
|
|
75
|
+
value: str
|
|
76
|
+
has_value: bool
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class RemoteEntry:
|
|
81
|
+
"""Separate directly configured strings from Git's rewritten destinations."""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
values: tuple[RemoteValue, ...]
|
|
85
|
+
fetch_urls: tuple[str, ...] = ()
|
|
86
|
+
push_urls: tuple[str, ...] = ()
|
|
87
|
+
active: bool = True
|
|
88
|
+
collision: bool = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class BranchTracking:
|
|
93
|
+
"""Keep a local branch identity and Git's upstream independently of config multiplicity."""
|
|
94
|
+
|
|
95
|
+
full_ref: str
|
|
96
|
+
oid: str
|
|
97
|
+
values: tuple[RemoteValue, ...]
|
|
98
|
+
upstream: str = ""
|
|
99
|
+
observed: bool = False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class RemoteSnapshot:
|
|
104
|
+
"""Bind config entries and their source fingerprints to one repository view.
|
|
105
|
+
|
|
106
|
+
``head_oid`` and ``head_ref`` are HEAD as the capture itself observed it, between the same reference signatures
|
|
107
|
+
as the branches: ``head_ref`` is a full branch name when HEAD is on a branch, born or not, and an empty string
|
|
108
|
+
when HEAD is detached. None means no observation was made, never a detached HEAD.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
context: RepositoryContext
|
|
112
|
+
git_filename: str
|
|
113
|
+
generation: int
|
|
114
|
+
entry_by_name: dict[str, RemoteEntry]
|
|
115
|
+
values: tuple[RemoteValue, ...]
|
|
116
|
+
direct_values: tuple[RemoteValue, ...]
|
|
117
|
+
signature: tuple
|
|
118
|
+
references: bytes = b""
|
|
119
|
+
tracking_by_ref: dict[str, BranchTracking] = field(default_factory=dict)
|
|
120
|
+
reference_state: tuple = ()
|
|
121
|
+
head_oid: str | None = None
|
|
122
|
+
head_ref: str | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
INACTIVE_REMOTE_SECTION = "-remote."
|
|
126
|
+
# The two tracking settings of a branch, also the last part of its ``branch.<name>.<field>`` configuration keys.
|
|
127
|
+
REMOTE_TRACKING_FIELD = "remote"
|
|
128
|
+
MERGE_TRACKING_FIELD = "merge"
|
|
129
|
+
# Git only accepts a remote named on the command line when one of its keys comes from the repository's own files.
|
|
130
|
+
REPOSITORY_CONFIG_SCOPES = (GitConfigScope.LOCAL, GitConfigScope.WORKTREE)
|
|
131
|
+
REMOTE_TRANSPORT_FIELDS = ("proxy", "proxyauthmethod", "receivepack", "uploadpack")
|
|
132
|
+
GIT_BOOLEAN_MAXIMUM_INT = 2147483647
|
|
133
|
+
GIT_INTEGER_PATTERN = re.compile(r"[ \t\n\r\v\f]*(?P<sign>[+-]?)(?P<digits>0x[0-9a-f]+|0[0-7]*|[1-9][0-9]*)(?P<unit>[kmg]?)")
|
|
134
|
+
GIT_INTEGER_FACTOR_BY_UNIT = {"": 1, "k": 1024, "m": 1024 ** 2, "g": 1024 ** 3}
|
|
135
|
+
# Two integer spellings mean different things to different Git builds, whatever their version number says. Git parses
|
|
136
|
+
# numbers with the C library's strtoimax: one that follows C23 (glibc 2.38 and later, as on Ubuntu 26) reads a 0b
|
|
137
|
+
# prefix as a binary number, while others stop at the 0 and refuse the rest. And recent Git releases accept the most
|
|
138
|
+
# negative int that older ones (Git for Windows 2.47) refuse. GitPane cannot have one grammar that matches both, so it
|
|
139
|
+
# refuses these spellings as invalid booleans rather than guess what the installed Git makes of them.
|
|
140
|
+
GIT_BINARY_INTEGER_PREFIX_PATTERN = re.compile(r"[ \t\n\r\v\f]*[+-]?0b")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def selected_endpoint_arguments(snapshot: RemoteSnapshot, name: str, endpoint: str, alias: str) -> list[str]:
|
|
144
|
+
"""Build a private single-URL remote preserving only its effective transport settings."""
|
|
145
|
+
arguments = ["-c", "remote." + alias + ".url=" + endpoint, "-c", "remote." + alias + ".pushurl=" + endpoint,
|
|
146
|
+
"-c", "remote." + alias + ".mirror=false"]
|
|
147
|
+
for field_name in REMOTE_TRANSPORT_FIELDS:
|
|
148
|
+
values = field_values(snapshot, name, field_name) if name else ()
|
|
149
|
+
if values:
|
|
150
|
+
*_, value = values
|
|
151
|
+
arguments.extend(["-c", "remote." + alias + "." + field_name + "=" + value.value])
|
|
152
|
+
return arguments
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def tracking_values(snapshot: RemoteSnapshot, full_ref: str, field: str) -> tuple[RemoteValue, ...]:
|
|
156
|
+
"""Return every occurrence of one exact branch field, including inherited entries."""
|
|
157
|
+
key = "branch." + full_ref.removeprefix(LOCAL_PREFIX) + "." + field
|
|
158
|
+
return tuple(value for value in snapshot.values if value.key == key)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def remote_section(snapshot: RemoteSnapshot, name: str) -> str:
|
|
162
|
+
"""Choose the captured section without inferring state from widget presentation."""
|
|
163
|
+
entry = snapshot.entry_by_name.get(name)
|
|
164
|
+
return (INACTIVE_REMOTE_SECTION if entry is not None and not entry.active else "remote.") + name
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def field_values(snapshot: RemoteSnapshot, name: str, field: str, *, direct: bool = False) -> tuple[RemoteValue, ...]:
|
|
168
|
+
"""Select all occurrences of a field without dropping scope or source identity."""
|
|
169
|
+
values: list[RemoteValue] = []
|
|
170
|
+
for value in snapshot.direct_values if direct else snapshot.values:
|
|
171
|
+
if value.key == remote_section(snapshot, name) + "." + field:
|
|
172
|
+
values.append(value)
|
|
173
|
+
return tuple(values)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse_config_boolean(value: RemoteValue, description: str) -> bool:
|
|
177
|
+
"""Interpret native boolean syntax without losing valueless keys or the caller's diagnostic.
|
|
178
|
+
|
|
179
|
+
A spelling whose meaning depends on how the installed Git was built (a binary ``0b`` number, or the most negative
|
|
180
|
+
int) is refused as an invalid boolean.
|
|
181
|
+
"""
|
|
182
|
+
if not value.has_value:
|
|
183
|
+
return True
|
|
184
|
+
# Native Git compares ASCII bytes; Unicode case folding would accept lookalike keywords and suffixes.
|
|
185
|
+
if not value.value.isascii():
|
|
186
|
+
raise ValueError("Invalid boolean for " + description)
|
|
187
|
+
text = value.value.casefold()
|
|
188
|
+
if text in ("true", "yes", "on"):
|
|
189
|
+
return True
|
|
190
|
+
if text in ("", "false", "no", "off"):
|
|
191
|
+
return False
|
|
192
|
+
if GIT_BINARY_INTEGER_PREFIX_PATTERN.match(text):
|
|
193
|
+
raise ValueError("Invalid boolean for " + description)
|
|
194
|
+
match = GIT_INTEGER_PATTERN.fullmatch(text)
|
|
195
|
+
if match is None:
|
|
196
|
+
raise ValueError("Invalid boolean for " + description)
|
|
197
|
+
digits = match.group("digits")
|
|
198
|
+
base = 16 if digits.startswith("0x") else 8 if digits.startswith("0") else 10
|
|
199
|
+
# Git parses C integers with base detection and a signed-int bound before accepting a boolean.
|
|
200
|
+
try:
|
|
201
|
+
number = int(digits, base) * GIT_INTEGER_FACTOR_BY_UNIT[match.group("unit")]
|
|
202
|
+
except ValueError as error:
|
|
203
|
+
raise ValueError("Invalid boolean for " + description) from error
|
|
204
|
+
# The most negative int is past this bound too, so it is refused like the other spellings Git builds disagree on.
|
|
205
|
+
if number > GIT_BOOLEAN_MAXIMUM_INT:
|
|
206
|
+
raise ValueError("Invalid boolean for " + description)
|
|
207
|
+
return number != 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def remote_boolean(snapshot: RemoteSnapshot, name: str, field: str, *, aliases: tuple[str, ...] = ()) -> bool:
|
|
211
|
+
"""Read the last native boolean occurrence, including aliases in their original config order."""
|
|
212
|
+
key_set: set[str] = set()
|
|
213
|
+
for field_name in (field, *aliases):
|
|
214
|
+
key_set.add(remote_section(snapshot, name) + "." + field_name)
|
|
215
|
+
values: list[RemoteValue] = []
|
|
216
|
+
for value in snapshot.values:
|
|
217
|
+
if value.key in key_set:
|
|
218
|
+
values.append(value)
|
|
219
|
+
if not values:
|
|
220
|
+
return False
|
|
221
|
+
last_index = len(values) - 1
|
|
222
|
+
return parse_config_boolean(values[last_index], "remote " + name + ": " + field)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def fetch_branch_refspecs(snapshot: RemoteSnapshot, name: str) -> tuple[str, ...]:
|
|
226
|
+
"""Keep configured branch mappings inside their remote namespace and exclude local tags."""
|
|
227
|
+
if remote_boolean(snapshot, name, "mirror"):
|
|
228
|
+
raise ValueError("Remote " + name + " is configured as a mirror. Adjust its configuration before fetching branches.")
|
|
229
|
+
refspecs: list[str] = []
|
|
230
|
+
namespace = REMOTE_PREFIX + name + "/"
|
|
231
|
+
for other_name in snapshot.entry_by_name:
|
|
232
|
+
if other_name != name and (other_name.startswith(name + "/") or name.startswith(other_name + "/")):
|
|
233
|
+
raise ValueError("Remote " + name + " overlaps the reference namespace of " + other_name + ". Adjust these remote names before fetching.")
|
|
234
|
+
for value in field_values(snapshot, name, "fetch"):
|
|
235
|
+
refspec = value.value
|
|
236
|
+
source, separator, destination = refspec.removeprefix("+").partition(":")
|
|
237
|
+
if source.startswith(TAG_PREFIX) and destination.startswith(TAG_PREFIX):
|
|
238
|
+
continue
|
|
239
|
+
negative = source.startswith("^")
|
|
240
|
+
if negative:
|
|
241
|
+
source = source.removeprefix("^")
|
|
242
|
+
if (not source.startswith(LOCAL_PREFIX) or any(character in refspec for character in ("\0", "\n", "\r"))
|
|
243
|
+
or (negative and separator) or (not negative and (not separator or not destination.startswith(namespace)))
|
|
244
|
+
or source.count("*") > 1 or (not negative and source.count("*") != destination.count("*"))):
|
|
245
|
+
raise ValueError("Remote " + name + " has an incompatible Fetch refspec. Adjust it to map branches only into " + namespace)
|
|
246
|
+
if refspec not in refspecs:
|
|
247
|
+
refspecs.append(refspec)
|
|
248
|
+
if not any(not refspec.startswith("^") for refspec in refspecs):
|
|
249
|
+
raise ValueError("Remote " + name + " has no configured branch Fetch refspec. Adjust it in Git configuration before fetching.")
|
|
250
|
+
return tuple(sorted(refspecs))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def refspec_pattern_match(pattern: str, full_ref: str) -> str | None:
|
|
254
|
+
"""Match one refspec side the way Git does: exact name, or one '*' standing for any text; return that text or None."""
|
|
255
|
+
if "*" not in pattern:
|
|
256
|
+
return "" if pattern == full_ref else None
|
|
257
|
+
prefix, _star, suffix = pattern.partition("*")
|
|
258
|
+
if len(full_ref) < len(prefix) + len(suffix) or not full_ref.startswith(prefix) or not full_ref.endswith(suffix):
|
|
259
|
+
return None
|
|
260
|
+
return full_ref[len(prefix):len(full_ref) - len(suffix)]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def remote_tracking_ref(snapshot: RemoteSnapshot, remote_name: str, server_ref: str) -> str:
|
|
264
|
+
"""Name the local remote-tracking branch that Git keeps for one server branch, or "" when there is none.
|
|
265
|
+
|
|
266
|
+
Git maps a server branch through the remote's Fetch refspecs: a matching negative refspec excludes it, otherwise the
|
|
267
|
+
first matching refspec in configuration order wins. Only a remote whose refspecs map branches inside its own
|
|
268
|
+
refs/remotes/<name>/ namespace is used, so the result can never point at a local branch or a tag.
|
|
269
|
+
"""
|
|
270
|
+
if not remote_name or not server_ref.startswith(LOCAL_PREFIX):
|
|
271
|
+
return ""
|
|
272
|
+
try:
|
|
273
|
+
fetch_branch_refspecs(snapshot, remote_name)
|
|
274
|
+
except ValueError:
|
|
275
|
+
return ""
|
|
276
|
+
refspecs: list[str] = []
|
|
277
|
+
for value in field_values(snapshot, remote_name, "fetch"):
|
|
278
|
+
refspecs.append(value.value)
|
|
279
|
+
for refspec in refspecs:
|
|
280
|
+
if refspec.startswith("^") and refspec_pattern_match(refspec.removeprefix("^"), server_ref) is not None:
|
|
281
|
+
return ""
|
|
282
|
+
for refspec in refspecs:
|
|
283
|
+
if refspec.startswith("^"):
|
|
284
|
+
continue
|
|
285
|
+
source, separator, destination = refspec.removeprefix("+").partition(":")
|
|
286
|
+
if not separator or not destination.startswith(REMOTE_PREFIX + remote_name + "/"):
|
|
287
|
+
continue
|
|
288
|
+
matched = refspec_pattern_match(source, server_ref)
|
|
289
|
+
if matched is not None:
|
|
290
|
+
return destination.replace("*", matched, 1)
|
|
291
|
+
return ""
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def remote_reference_oid(snapshot: RemoteSnapshot, full_ref: str) -> str:
|
|
295
|
+
"""Return the object a captured remote-tracking reference points to, or "" when it is absent."""
|
|
296
|
+
target = full_ref.encode("utf-8", "surrogateescape")
|
|
297
|
+
for record in snapshot.references.splitlines():
|
|
298
|
+
name, _separator, rest = record.partition(b"\0")
|
|
299
|
+
if name == target:
|
|
300
|
+
oid, _separator, _symref = rest.partition(b"\0")
|
|
301
|
+
return oid.decode("ascii")
|
|
302
|
+
return ""
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def refspec_write_refs(refspecs: tuple[str, ...]) -> tuple[str, ...]:
|
|
306
|
+
"""Capture local destinations conservatively when a mapping cannot be understood."""
|
|
307
|
+
reference_set = set()
|
|
308
|
+
for refspec in refspecs:
|
|
309
|
+
if refspec.startswith("^"):
|
|
310
|
+
continue
|
|
311
|
+
source, separator, destination = refspec.removeprefix("+").partition(":")
|
|
312
|
+
if (not separator or not source.startswith(REFERENCE_ROOT_PREFIX) or not destination.startswith(REFERENCE_ROOT_PREFIX)
|
|
313
|
+
or source.count("*") > 1 or source.count("*") != destination.count("*")
|
|
314
|
+
or any(character in refspec for character in ("\0", "\n", "\r"))):
|
|
315
|
+
return ("refs/*",)
|
|
316
|
+
reference_set.add(destination)
|
|
317
|
+
return tuple(sorted(reference_set))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def fetch_remote_name(snapshot: RemoteSnapshot, branch_ref: str | None) -> str:
|
|
321
|
+
"""Resolve branch tracking or Git's origin fallback without choosing an arbitrary remote."""
|
|
322
|
+
names: list[str] = []
|
|
323
|
+
for name in sorted(snapshot.entry_by_name):
|
|
324
|
+
if snapshot.entry_by_name[name].active:
|
|
325
|
+
names.append(name)
|
|
326
|
+
if branch_ref and branch_ref.startswith(LOCAL_PREFIX):
|
|
327
|
+
values = tracking_values(snapshot, branch_ref, REMOTE_TRACKING_FIELD)
|
|
328
|
+
if values:
|
|
329
|
+
name_set = {value.value for value in values}
|
|
330
|
+
if len(name_set) == 1:
|
|
331
|
+
name = next(iter(name_set))
|
|
332
|
+
return name if name in names else ""
|
|
333
|
+
return ""
|
|
334
|
+
if branch_ref and branch_ref.startswith(REMOTE_PREFIX):
|
|
335
|
+
matches = []
|
|
336
|
+
for name in names:
|
|
337
|
+
if branch_ref.startswith(REMOTE_PREFIX + name + "/"):
|
|
338
|
+
matches.append(name)
|
|
339
|
+
if len(matches) == 1:
|
|
340
|
+
name, = matches
|
|
341
|
+
return name
|
|
342
|
+
return ""
|
|
343
|
+
if "origin" in names:
|
|
344
|
+
return "origin"
|
|
345
|
+
if len(names) == 1:
|
|
346
|
+
name, = names
|
|
347
|
+
return name
|
|
348
|
+
return ""
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def default_push_destination_name(snapshot: RemoteSnapshot, source_ref: str | None, remote_name: str | None) -> str:
|
|
352
|
+
"""Name the remote branch Push proposes for a local branch: the branch it tracks on that remote, or its own name when it tracks nothing.
|
|
353
|
+
|
|
354
|
+
A branch that tracks something else (another remote, several branches, a remote without a branch) gets no
|
|
355
|
+
proposal, so the user chooses. The Push window and Commit & push both read their proposal here, so they agree.
|
|
356
|
+
"""
|
|
357
|
+
if not source_ref or not source_ref.startswith(LOCAL_PREFIX):
|
|
358
|
+
return ""
|
|
359
|
+
merges = tracking_values(snapshot, source_ref, MERGE_TRACKING_FIELD)
|
|
360
|
+
remotes = tracking_values(snapshot, source_ref, REMOTE_TRACKING_FIELD)
|
|
361
|
+
if not merges and not remotes:
|
|
362
|
+
return source_ref.removeprefix(LOCAL_PREFIX)
|
|
363
|
+
if len(merges) != 1 or len(remotes) != 1:
|
|
364
|
+
return ""
|
|
365
|
+
merge, = merges
|
|
366
|
+
remote, = remotes
|
|
367
|
+
if remote.value != remote_name or not merge.value.startswith(LOCAL_PREFIX):
|
|
368
|
+
return ""
|
|
369
|
+
return merge.value.removeprefix(LOCAL_PREFIX)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def matched_rewrite(url: str, values: tuple[RemoteValue, ...], suffix: str = ".insteadof") -> str | None:
|
|
373
|
+
"""Apply Git's longest matching rewrite prefix, or answer None when no rule of that kind matches the URL."""
|
|
374
|
+
matching_values: list[RemoteValue] = []
|
|
375
|
+
for value in values:
|
|
376
|
+
if value.key.startswith("url.") and value.key.endswith(suffix) and url.startswith(value.value):
|
|
377
|
+
matching_values.append(value)
|
|
378
|
+
if not matching_values:
|
|
379
|
+
return None
|
|
380
|
+
longest = max(len(value.value) for value in matching_values)
|
|
381
|
+
destination_set: set[str] = set()
|
|
382
|
+
for value in matching_values:
|
|
383
|
+
if len(value.value) == longest:
|
|
384
|
+
destination_set.add(value.key.removeprefix("url.").removesuffix(suffix) + url[len(value.value):])
|
|
385
|
+
if len(destination_set) != 1:
|
|
386
|
+
raise ValueError("Conflicting URL rewrite rules; review Git configuration before saving")
|
|
387
|
+
return destination_set.pop()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def rewrite_url(url: str, values: tuple[RemoteValue, ...], suffix: str = ".insteadof") -> str:
|
|
391
|
+
"""Apply Git's longest insteadOf prefix to compare raw and expanded URL identities."""
|
|
392
|
+
rewritten = matched_rewrite(url, values, suffix)
|
|
393
|
+
return url if rewritten is None else rewritten
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def active_remote_urls(name: str, entry_values: tuple[RemoteValue, ...], values: tuple[RemoteValue, ...]) -> tuple[tuple[str, ...], tuple[str, ...]] | None:
|
|
397
|
+
"""Answer what ``git remote get-url --all`` and ``--push --all`` print for one active remote, from its config values.
|
|
398
|
+
|
|
399
|
+
Git only knows a remote named on its command line when one of its keys comes from the repository's own files, so
|
|
400
|
+
a remote defined only globally or on the command line has no destination. An empty URL clears the URLs listed
|
|
401
|
+
before it, Fetch URLs go through the longest insteadOf rule and explicit Push URLs too; without an explicit Push
|
|
402
|
+
URL, the Fetch URLs a pushInsteadOf rule matches become the Push URLs, and failing any match the Fetch URLs are
|
|
403
|
+
used. Answers None for the cases whose result only Git can establish: a valueless URL or rewrite rule, which Git
|
|
404
|
+
refuses, no URL left, where Git falls back to the remote's name or to legacy remote files, a name Git ignores,
|
|
405
|
+
and two equally long rewrite rules with different destinations, where Git applies its own rule order.
|
|
406
|
+
"""
|
|
407
|
+
if name.startswith("/"):
|
|
408
|
+
return None
|
|
409
|
+
if not any(value.scope in REPOSITORY_CONFIG_SCOPES for value in entry_values):
|
|
410
|
+
return (), ()
|
|
411
|
+
url_by_field: dict[str, list[str]] = {"url": [], "pushurl": []}
|
|
412
|
+
prefix = "remote." + name + "."
|
|
413
|
+
for value in entry_values:
|
|
414
|
+
urls = url_by_field.get(value.key.removeprefix(prefix))
|
|
415
|
+
if urls is None:
|
|
416
|
+
continue
|
|
417
|
+
if not value.has_value:
|
|
418
|
+
return None
|
|
419
|
+
if value.value:
|
|
420
|
+
urls.append(value.value)
|
|
421
|
+
else:
|
|
422
|
+
urls.clear()
|
|
423
|
+
fetch_raw_urls, push_raw_urls = url_by_field["url"], url_by_field["pushurl"]
|
|
424
|
+
if not fetch_raw_urls:
|
|
425
|
+
return None
|
|
426
|
+
for value in values:
|
|
427
|
+
if value.key.startswith("url.") and value.key.endswith((".insteadof", ".pushinsteadof")) and not value.has_value:
|
|
428
|
+
return None
|
|
429
|
+
try:
|
|
430
|
+
fetch_urls: list[str] = []
|
|
431
|
+
for url in fetch_raw_urls:
|
|
432
|
+
fetch_urls.append(rewrite_url(url, values))
|
|
433
|
+
push_urls: list[str] = []
|
|
434
|
+
if push_raw_urls:
|
|
435
|
+
for url in push_raw_urls:
|
|
436
|
+
push_urls.append(rewrite_url(url, values))
|
|
437
|
+
else:
|
|
438
|
+
for url in fetch_raw_urls:
|
|
439
|
+
rewritten = matched_rewrite(url, values, ".pushinsteadof")
|
|
440
|
+
if rewritten is not None:
|
|
441
|
+
push_urls.append(rewritten)
|
|
442
|
+
if not push_urls:
|
|
443
|
+
push_urls = fetch_urls
|
|
444
|
+
except ValueError:
|
|
445
|
+
return None
|
|
446
|
+
return tuple(fetch_urls), tuple(push_urls)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def parse_remote_reference_output(output: bytes) -> tuple[bytes, dict[str, tuple[str, str]], str | None, str | None]:
|
|
450
|
+
"""Split one REFERENCE_FORMAT listing of branches and remote-tracking branches, whatever its line order.
|
|
451
|
+
|
|
452
|
+
The listing goes through the shared reference parser, so a cut, duplicated or doubly marked listing is refused
|
|
453
|
+
exactly as it is for the reference views, and HEAD is only named when that parser proves it; each local branch's
|
|
454
|
+
upstream comes from that same parse. Answers the remote-tracking branches in their historical
|
|
455
|
+
``name NUL object NUL symbolic target`` lines sorted by name, each local branch's object and upstream (empty when it
|
|
456
|
+
has none), and HEAD's commit and branch or None for both.
|
|
457
|
+
"""
|
|
458
|
+
read = parse_reference_read(output)
|
|
459
|
+
remote_record_by_name: dict[bytes, bytes] = {}
|
|
460
|
+
branch_state_by_ref: dict[str, tuple[str, str]] = {}
|
|
461
|
+
for reference in read.references:
|
|
462
|
+
object_oid = read.object_oid_by_name[reference.full_name]
|
|
463
|
+
if reference.full_name.startswith(REMOTE_PREFIX):
|
|
464
|
+
encoded_name = reference.full_name.encode("utf-8", "surrogateescape")
|
|
465
|
+
symbolic_target = (reference.symbolic_target or "").encode("utf-8", "surrogateescape")
|
|
466
|
+
remote_record_by_name[encoded_name] = encoded_name + b"\0" + object_oid.encode("ascii") + b"\0" + symbolic_target + REFERENCE_LINE_TERMINATOR
|
|
467
|
+
elif reference.full_name.startswith(LOCAL_PREFIX):
|
|
468
|
+
branch_state_by_ref[reference.full_name] = (object_oid, read.upstream_by_name.get(reference.full_name, ""))
|
|
469
|
+
else:
|
|
470
|
+
raise ValueError("Git listed a reference outside branches and remote-tracking branches")
|
|
471
|
+
remote_records: list[bytes] = []
|
|
472
|
+
# Git sorts by the bytes of the full name; sorting here keeps the historical stream independent of the listing order.
|
|
473
|
+
for encoded_name in sorted(remote_record_by_name):
|
|
474
|
+
remote_records.append(remote_record_by_name[encoded_name])
|
|
475
|
+
sorted_branch_state_by_ref: dict[str, tuple[str, str]] = {}
|
|
476
|
+
for full_ref in sorted(branch_state_by_ref):
|
|
477
|
+
sorted_branch_state_by_ref[full_ref] = branch_state_by_ref[full_ref]
|
|
478
|
+
return b"".join(remote_records), sorted_branch_state_by_ref, read.head_oid, read.head_ref
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def config_records(output: bytes) -> tuple[ConfigRecord, ...]:
|
|
482
|
+
"""Split NUL-delimited scope, origin and key/value records into their texts, without deduplication."""
|
|
483
|
+
if not output:
|
|
484
|
+
return ()
|
|
485
|
+
if not output.endswith(b"\0"):
|
|
486
|
+
raise ValueError("Incomplete remote configuration")
|
|
487
|
+
fields = iter(output.removesuffix(b"\0").split(b"\0"))
|
|
488
|
+
records: list[ConfigRecord] = []
|
|
489
|
+
try:
|
|
490
|
+
for scope in fields:
|
|
491
|
+
origin, record = next(fields), next(fields)
|
|
492
|
+
key, separator, value = record.partition(b"\n")
|
|
493
|
+
records.append(ConfigRecord(scope.decode("utf-8", "surrogateescape"), origin.decode("utf-8", "surrogateescape"), key.decode("utf-8", "surrogateescape"),
|
|
494
|
+
value.decode("utf-8", "surrogateescape") if separator else "", bool(separator)))
|
|
495
|
+
except StopIteration as error:
|
|
496
|
+
raise ValueError("Incomplete remote configuration record") from error
|
|
497
|
+
return tuple(records)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def parse_config(output: bytes) -> tuple[RemoteValue, ...]:
|
|
501
|
+
"""Decode NUL-delimited scope, origin and key/value records without deduplication, each scope read as its member."""
|
|
502
|
+
values: list[RemoteValue] = []
|
|
503
|
+
for record in config_records(output):
|
|
504
|
+
values.append(RemoteValue(record.key, record.value, config_scope(record.scope_text), record.origin, record.has_value))
|
|
505
|
+
return tuple(values)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def config_signature(context: RepositoryContext, values: tuple[RemoteValue, ...]) -> tuple:
|
|
509
|
+
"""Detect changed or newly created config sources while a write is queued."""
|
|
510
|
+
filename_set = {os.path.join(context.common_git_dir, "config"), os.path.join(context.git_dir, "config.worktree")}
|
|
511
|
+
identity_records: list[tuple] = []
|
|
512
|
+
for directory in (context.repository_dir, context.git_dir, context.common_git_dir):
|
|
513
|
+
metadata = os.stat(directory)
|
|
514
|
+
identity_records.append((os.path.realpath(directory), metadata.st_dev, metadata.st_ino))
|
|
515
|
+
dot_git_filename = os.path.join(context.repository_dir, ".git")
|
|
516
|
+
if os.path.isfile(dot_git_filename):
|
|
517
|
+
filename_set.add(dot_git_filename)
|
|
518
|
+
for variable in ("GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM"):
|
|
519
|
+
filename = os.environ.get(variable)
|
|
520
|
+
if filename:
|
|
521
|
+
filename_set.add(filename)
|
|
522
|
+
if not os.environ.get("GIT_CONFIG_GLOBAL"):
|
|
523
|
+
for filename in (os.path.join(os.path.expanduser("~"), ".gitconfig"), os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")), "git", "config")):
|
|
524
|
+
filename_set.add(filename)
|
|
525
|
+
for value in values:
|
|
526
|
+
if not value.origin.startswith("file:"):
|
|
527
|
+
continue
|
|
528
|
+
filename = value.origin.removeprefix("file:")
|
|
529
|
+
if not os.path.isabs(filename):
|
|
530
|
+
filename = os.path.join(context.repository_dir, filename)
|
|
531
|
+
filename_set.add(filename)
|
|
532
|
+
if (value.key == "include.path" or value.key.startswith("includeif.") and value.key.endswith(".path")) and value.value:
|
|
533
|
+
included_filename = os.path.expanduser(value.value)
|
|
534
|
+
if not os.path.isabs(included_filename):
|
|
535
|
+
included_filename = os.path.join(os.path.dirname(filename), included_filename)
|
|
536
|
+
filename_set.add(included_filename)
|
|
537
|
+
records: list[tuple] = []
|
|
538
|
+
for filename in sorted(filename_set):
|
|
539
|
+
try:
|
|
540
|
+
with open(filename, "rb") as stream:
|
|
541
|
+
records.append((filename, os.fstat(stream.fileno()).st_ino, stream.read()))
|
|
542
|
+
except FileNotFoundError:
|
|
543
|
+
records.append((filename, None, None))
|
|
544
|
+
return tuple(identity_records), tuple(records)
|
gitpane/ui/__init__.py
ADDED
|
File without changes
|
|
File without changes
|