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,341 @@
|
|
|
1
|
+
"""Hold the values every reader of a repository shares, from its context to its commits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import TYPE_CHECKING, Self
|
|
9
|
+
|
|
10
|
+
from gitpane.config import LOCAL_PREFIX, REMOTE_PREFIX, TAG_PREFIX
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from gitpane.reference.reference_snapshot import ReferenceSnapshot
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class RequestIdentity:
|
|
18
|
+
"""Identify a window and its current read to reject superseded responses."""
|
|
19
|
+
|
|
20
|
+
session_id: str
|
|
21
|
+
generation: int = 0
|
|
22
|
+
selection_generation: int = 0
|
|
23
|
+
oid: str | None = None
|
|
24
|
+
search_generation: int = 0
|
|
25
|
+
relation_generation: int = 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FilePanel(Enum):
|
|
29
|
+
"""The place a read of one file or file list fills, which a new read of the same place replaces and a retry reads again.
|
|
30
|
+
|
|
31
|
+
Four are panels of a repository window: the diff, the file tree, the file content and the history of one file. The
|
|
32
|
+
last two are not panels but reads of their own: the files handed to an external comparison tool, and a file exported to disk.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
DIFF = "diff"
|
|
36
|
+
TREE = "tree"
|
|
37
|
+
BLOB = "blob"
|
|
38
|
+
FILE_HISTORY = "file_history"
|
|
39
|
+
DIFFTOOL = "difftool"
|
|
40
|
+
EXPORT = "export"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class FileReadIdentity:
|
|
45
|
+
"""Keep a panel's comparison independent of the commit detail request."""
|
|
46
|
+
|
|
47
|
+
request_identity: RequestIdentity
|
|
48
|
+
panel_kind: FilePanel = FilePanel.DIFF
|
|
49
|
+
panel_generation: int = 0
|
|
50
|
+
parent_oid: str | None = None
|
|
51
|
+
old_path: bytes | None = None
|
|
52
|
+
new_path: bytes | None = None
|
|
53
|
+
full_context: bool = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FileStatus(Enum):
|
|
57
|
+
"""Change Git reports for one file, by the status letter it writes in raw diffs, name-status lists and status records."""
|
|
58
|
+
|
|
59
|
+
ADDED = "A"
|
|
60
|
+
"""The file exists only on the new side, or is not tracked yet."""
|
|
61
|
+
BROKEN = "B"
|
|
62
|
+
"""Git broke the pairing of a heavily rewritten file into a deletion and an addition."""
|
|
63
|
+
COPIED = "C"
|
|
64
|
+
"""The file is a copy of another one."""
|
|
65
|
+
DELETED = "D"
|
|
66
|
+
"""The file exists only on the old side."""
|
|
67
|
+
MODIFIED = "M"
|
|
68
|
+
"""The content or the mode of the file changed."""
|
|
69
|
+
RENAMED = "R"
|
|
70
|
+
"""The file moved from another path."""
|
|
71
|
+
TYPE_CHANGED = "T"
|
|
72
|
+
"""The file changed type, between a regular file, a symbolic link and a submodule."""
|
|
73
|
+
UNMERGED = "U"
|
|
74
|
+
"""The file is in conflict."""
|
|
75
|
+
UNKNOWN = "X"
|
|
76
|
+
"""Git could not tell; it only writes this letter on an internal error."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Git writes the status letter first, followed by the similarity score of a rename or a copy (R100, C075).
|
|
80
|
+
STATUS_LETTER_LENGTH = 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def file_status(raw_status: str) -> FileStatus:
|
|
84
|
+
"""Read the status Git wrote for a file by its letter, the similarity score of a rename or a copy left aside.
|
|
85
|
+
|
|
86
|
+
A letter Git does not document raises ValueError, like any other output that cannot be read.
|
|
87
|
+
"""
|
|
88
|
+
return FileStatus(raw_status[:STATUS_LETTER_LENGTH])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class RevisionEntry:
|
|
93
|
+
"""Retain both sides of a historical change without consulting disk files."""
|
|
94
|
+
|
|
95
|
+
commit_oid: str
|
|
96
|
+
parent_oid: str | None
|
|
97
|
+
old_path: bytes | None
|
|
98
|
+
new_path: bytes | None
|
|
99
|
+
old_blob_oid: str | None
|
|
100
|
+
new_blob_oid: str | None
|
|
101
|
+
old_mode: str
|
|
102
|
+
new_mode: str
|
|
103
|
+
status: FileStatus
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class TreeEntry:
|
|
108
|
+
"""Retain a revision's literal Git path and object identity without local paths."""
|
|
109
|
+
|
|
110
|
+
path: bytes
|
|
111
|
+
mode: str
|
|
112
|
+
object_type: str
|
|
113
|
+
oid: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class HistoricalFileSelection:
|
|
118
|
+
"""Keep an immutable blob and its real historical side for file operations."""
|
|
119
|
+
|
|
120
|
+
commit_oid: str
|
|
121
|
+
parent_oid: str | None
|
|
122
|
+
path: bytes
|
|
123
|
+
blob_oid: str
|
|
124
|
+
mode: str
|
|
125
|
+
selection_identity: FileReadIdentity
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def historical_selection(entry: RevisionEntry | TreeEntry, identity: FileReadIdentity) -> HistoricalFileSelection:
|
|
129
|
+
"""Choose the after side, or the selected parent's before side for a deletion."""
|
|
130
|
+
if isinstance(entry, TreeEntry):
|
|
131
|
+
if identity.request_identity.oid is None:
|
|
132
|
+
raise ValueError("No historical commit selected")
|
|
133
|
+
return HistoricalFileSelection(identity.request_identity.oid, None, entry.path, entry.oid, entry.mode, identity)
|
|
134
|
+
if entry.new_path is not None and entry.new_blob_oid is not None:
|
|
135
|
+
return HistoricalFileSelection(entry.commit_oid, entry.parent_oid, entry.new_path, entry.new_blob_oid, entry.new_mode, identity)
|
|
136
|
+
if entry.parent_oid is not None and entry.old_path is not None and entry.old_blob_oid is not None:
|
|
137
|
+
return HistoricalFileSelection(entry.commit_oid, entry.parent_oid, entry.old_path, entry.old_blob_oid, entry.old_mode, identity)
|
|
138
|
+
raise ValueError("The selected entry has no historical file")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class BlobContentKind(Enum):
|
|
142
|
+
"""What the preview of a file version holds, which decides how the reader shows it."""
|
|
143
|
+
|
|
144
|
+
TEXT = "text"
|
|
145
|
+
"""Text decoded without error."""
|
|
146
|
+
EMPTY = "empty"
|
|
147
|
+
"""A file of zero bytes."""
|
|
148
|
+
BINARY = "binary"
|
|
149
|
+
"""Bytes with a NUL and no text byte order mark: not shown as text."""
|
|
150
|
+
ENCODING = "encoding"
|
|
151
|
+
"""Text in an unknown or invalid encoding, shown with replacement characters."""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class BlobContent:
|
|
156
|
+
"""Keep original bytes separate from the reader's display representation."""
|
|
157
|
+
|
|
158
|
+
oid: str
|
|
159
|
+
raw_bytes: bytes
|
|
160
|
+
display_text: str
|
|
161
|
+
content_kind: BlobContentKind
|
|
162
|
+
complete: bool
|
|
163
|
+
size_bytes: int = 0
|
|
164
|
+
diagnostic: str = ""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class ComparisonGroup:
|
|
169
|
+
"""Associate each changed file with the real parent used for comparison."""
|
|
170
|
+
|
|
171
|
+
parent_oid: str | None
|
|
172
|
+
parent_short_oid: str
|
|
173
|
+
parent_subject: str
|
|
174
|
+
entries: list[RevisionEntry]
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def file_count(self) -> int:
|
|
178
|
+
"""Count only the changes belonging to this parent comparison."""
|
|
179
|
+
return len(self.entries)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class RepositoryContext:
|
|
184
|
+
"""Describe Git-resolved repository locations including worktrees and bare repos."""
|
|
185
|
+
|
|
186
|
+
worktree_dir: str | None
|
|
187
|
+
git_dir: str
|
|
188
|
+
common_git_dir: str
|
|
189
|
+
is_bare: bool
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def repository_dir(self) -> str:
|
|
193
|
+
"""Choose the actual repository directory even when no working tree exists."""
|
|
194
|
+
return self.worktree_dir or self.git_dir
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True)
|
|
198
|
+
class Commit:
|
|
199
|
+
"""Keep complete Git identity, ancestry and readable commit metadata together."""
|
|
200
|
+
|
|
201
|
+
oid: str
|
|
202
|
+
short_oid: str
|
|
203
|
+
parent_oids: list[str]
|
|
204
|
+
author_name: str
|
|
205
|
+
author_email: str
|
|
206
|
+
author_timestamp: int
|
|
207
|
+
committer_timestamp: int
|
|
208
|
+
encoding: str
|
|
209
|
+
message: str
|
|
210
|
+
detail_error: str | None = None
|
|
211
|
+
body_preview: str = ""
|
|
212
|
+
# Git accepts commits whose time is missing or beyond what a date can hold, as some old imported histories have;
|
|
213
|
+
# such a commit keeps its message and author, and only its dates read as unavailable.
|
|
214
|
+
time_unavailable: bool = False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class ReferenceKind(Enum):
|
|
218
|
+
"""Group of a displayed reference, in the order the history and the graph list them."""
|
|
219
|
+
|
|
220
|
+
HEAD = ("head", "", "HEAD")
|
|
221
|
+
HEADS = ("heads", LOCAL_PREFIX, "Local branches")
|
|
222
|
+
REMOTES = ("remotes", REMOTE_PREFIX, "Remote branches")
|
|
223
|
+
TAGS = ("tags", TAG_PREFIX, "Tags")
|
|
224
|
+
|
|
225
|
+
def __new__(cls, value: str, prefix: str, label: str) -> Self:
|
|
226
|
+
"""Keep the group name as the value, with the namespace Git lists it under and the heading the window shows."""
|
|
227
|
+
member = object.__new__(cls)
|
|
228
|
+
member._value_ = value
|
|
229
|
+
member.prefix = prefix
|
|
230
|
+
member.label = label
|
|
231
|
+
return member
|
|
232
|
+
|
|
233
|
+
def __lt__(self, other: object) -> bool:
|
|
234
|
+
"""Order kinds as they are declared: HEAD, local branches, remote branches, tags.
|
|
235
|
+
|
|
236
|
+
Sorting references on their kind keeps the order the group names had when they were plain text. Another type of
|
|
237
|
+
object is left to Python, which tries the other side of the comparison and refuses it when neither knows.
|
|
238
|
+
"""
|
|
239
|
+
if not isinstance(other, ReferenceKind):
|
|
240
|
+
return NotImplemented
|
|
241
|
+
members = list(type(self))
|
|
242
|
+
return members.index(self) < members.index(other)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclass(frozen=True)
|
|
246
|
+
class Reference:
|
|
247
|
+
"""Keep a displayed reference and its peeled commit target, if one exists."""
|
|
248
|
+
|
|
249
|
+
full_name: str
|
|
250
|
+
kind: ReferenceKind
|
|
251
|
+
target_oid: str | None
|
|
252
|
+
symbolic_target: str | None = None
|
|
253
|
+
is_head: bool = False
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True)
|
|
257
|
+
class CommitLink:
|
|
258
|
+
"""Identify a direct relative with its Git abbreviation and full subject."""
|
|
259
|
+
|
|
260
|
+
oid: str
|
|
261
|
+
short_oid: str
|
|
262
|
+
subject: str
|
|
263
|
+
available: bool = True
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass(frozen=True)
|
|
267
|
+
class CommitRelation:
|
|
268
|
+
"""Separate incomplete child discovery from a proven absence of children."""
|
|
269
|
+
|
|
270
|
+
oid: str
|
|
271
|
+
parents: list[CommitLink]
|
|
272
|
+
child_links: list[CommitLink]
|
|
273
|
+
children_complete: bool
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass(frozen=True)
|
|
277
|
+
class OpeningHeadRead:
|
|
278
|
+
"""Remember the shallow flag and HEAD that one opening read, for the first history capture of that same request.
|
|
279
|
+
|
|
280
|
+
It is display data only and authorizes nothing. It belongs to one request of one window, with the Git and the
|
|
281
|
+
context it was read with, and is used at most once; any other opening, cancellation or closing drops it.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
identity: RequestIdentity
|
|
285
|
+
context: RepositoryContext
|
|
286
|
+
git_filename: str
|
|
287
|
+
shallow: bool
|
|
288
|
+
head_oid: str | None
|
|
289
|
+
head_ref: str | None = None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass
|
|
293
|
+
class HistorySnapshot:
|
|
294
|
+
"""Associate one stable reference capture with its progressively read ancestry."""
|
|
295
|
+
|
|
296
|
+
generation: int
|
|
297
|
+
references: list[Reference]
|
|
298
|
+
tip_oids: list[str]
|
|
299
|
+
head_oid: str | None
|
|
300
|
+
ordered_oids: list[str] = field(default_factory=list)
|
|
301
|
+
parent_oids_by_oid: dict[str, list[str]] = field(default_factory=dict)
|
|
302
|
+
child_oids_by_oid: dict[str, list[str]] = field(default_factory=dict)
|
|
303
|
+
complete: bool = False
|
|
304
|
+
shallow: bool = False
|
|
305
|
+
error: str | None = None
|
|
306
|
+
explicit_commits: list[Commit] = field(default_factory=list)
|
|
307
|
+
# The reference capture this history was built from, when one was shared with it; handed over to the window that
|
|
308
|
+
# finally shows this history so it does not list the same references again. Display data only.
|
|
309
|
+
reference_snapshot: ReferenceSnapshot | None = field(default=None, compare=False, repr=False)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# A full Git object ID: 40 hexadecimal digits in a SHA-1 repository, 64 in a SHA-256 one.
|
|
313
|
+
FULL_OID_PATTERN = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})")
|
|
314
|
+
# Suffix that makes Git follow a revision to the commit it leads to, and fail when it leads to none.
|
|
315
|
+
COMMIT_PEEL_SUFFIX = "^{commit}"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def validate_oid(oid: str) -> str:
|
|
319
|
+
"""Reject anything except a full Git object ID before using it as an argument."""
|
|
320
|
+
if FULL_OID_PATTERN.fullmatch(oid) is None:
|
|
321
|
+
raise ValueError("Invalid Git object ID")
|
|
322
|
+
return oid
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
SUCCESS_EXIT_CODE = 0
|
|
326
|
+
# Exit status of a cherry-pick, revert or rebase that stopped on a conflict. Any other failure status (128 for a fatal
|
|
327
|
+
# error, a crash) is a real failure even when it left unmerged entries behind.
|
|
328
|
+
GIT_CONFLICT_RETURN_CODE = 1
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dataclass(frozen=True)
|
|
332
|
+
class RepositoryFailure:
|
|
333
|
+
"""Preserve the concrete command failure for useful error presentation."""
|
|
334
|
+
|
|
335
|
+
operation: str
|
|
336
|
+
repository_dir: str
|
|
337
|
+
program_filename: str
|
|
338
|
+
arguments: list[str]
|
|
339
|
+
cause: str
|
|
340
|
+
stderr: bytes = b""
|
|
341
|
+
return_code: int | None = None
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Answer questions about commits from the history graph already held in memory, without starting Git."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from gitpane.config import COMMAND_LINE_CONTROL_CHARACTER_SET, LOCAL_PREFIX, REFERENCE_ROOT_PREFIX, REMOTE_PREFIX, TAG_PREFIX
|
|
9
|
+
from gitpane.history.repository_value import FULL_OID_PATTERN
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from gitpane.history.repository_value import Reference
|
|
13
|
+
|
|
14
|
+
# Signs and equality text of the Git Extensions commit counter shown beside a revision field ("-13", "+2", "-3+2", "=").
|
|
15
|
+
REMOVED_COMMIT_SIGN = "-"
|
|
16
|
+
ADDED_COMMIT_SIGN = "+"
|
|
17
|
+
SAME_COMMIT_TEXT = "="
|
|
18
|
+
|
|
19
|
+
# Why a typed revision is refused before any Git process: Git would read a leading dash as an option, and a control
|
|
20
|
+
# character cannot belong to a revision and could split what Git or the logs receive.
|
|
21
|
+
EMPTY_REVISION_CAUSE = "Enter a revision"
|
|
22
|
+
OPTION_REVISION_CAUSE = "A revision cannot start with '-'"
|
|
23
|
+
CONTROL_CHARACTER_REVISION_CAUSE = "A revision cannot contain control characters"
|
|
24
|
+
REVISION_OPTION_PREFIX = "-"
|
|
25
|
+
HEAD_REVISION = "HEAD"
|
|
26
|
+
# The symbolic reference a remote keeps for its default branch, which a bare remote name designates.
|
|
27
|
+
REMOTE_DEFAULT_BRANCH_SUFFIX = "/HEAD"
|
|
28
|
+
# Names Git first looks up as files beside HEAD in the repository directory (FETCH_HEAD, ORIG_HEAD, MERGE_HEAD...).
|
|
29
|
+
# They are not in the reference listing, so such a name is always left to Git.
|
|
30
|
+
ROOT_REFERENCE_NAME_PATTERN = re.compile(r"[A-Z_]+")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ancestor_oid_set(start_oid: str, parent_oids_by_oid: dict[str, list[str]]) -> set[str]:
|
|
34
|
+
"""Return a commit and every ancestor reachable through the known parent links.
|
|
35
|
+
|
|
36
|
+
The walk is iterative so a long linear history cannot overflow Python's call stack; parents missing from the
|
|
37
|
+
mapping (a shallow boundary, or a graph still being read) simply end that branch of the walk.
|
|
38
|
+
"""
|
|
39
|
+
visited_oid_set: set[str] = set()
|
|
40
|
+
pending_oids = [start_oid]
|
|
41
|
+
while pending_oids:
|
|
42
|
+
oid = pending_oids.pop()
|
|
43
|
+
if oid in visited_oid_set:
|
|
44
|
+
continue
|
|
45
|
+
visited_oid_set.add(oid)
|
|
46
|
+
pending_oids.extend(parent_oids_by_oid.get(oid, []))
|
|
47
|
+
return visited_oid_set
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def unmerged_branch_refs_in_memory(branch_oid_by_ref: dict[str, str], base_oid_by_ref: dict[str, str | None], parent_oids_by_oid: dict[str, list[str]],
|
|
51
|
+
graph_complete: bool) -> tuple[str, ...] | None:
|
|
52
|
+
"""Tell which branches are not merged into their base, from the history already in memory, or None when it cannot tell.
|
|
53
|
+
|
|
54
|
+
A branch is merged when its commit is its base or one of the base's ancestors. A branch without a base is never
|
|
55
|
+
merged, as in Git. None means memory is not enough: the graph is still being read, or a base commit is not in it,
|
|
56
|
+
so the caller has to ask Git. The answer keeps the order of ``branch_oid_by_ref``. The ancestors of each distinct
|
|
57
|
+
base are walked once, however many branches share it.
|
|
58
|
+
"""
|
|
59
|
+
if not graph_complete:
|
|
60
|
+
return None
|
|
61
|
+
ancestor_oid_set_by_base_oid: dict[str, set[str]] = {}
|
|
62
|
+
for base_oid in base_oid_by_ref.values():
|
|
63
|
+
if base_oid is None or base_oid in ancestor_oid_set_by_base_oid:
|
|
64
|
+
continue
|
|
65
|
+
if base_oid not in parent_oids_by_oid:
|
|
66
|
+
return None
|
|
67
|
+
ancestor_oid_set_by_base_oid[base_oid] = ancestor_oid_set(base_oid, parent_oids_by_oid)
|
|
68
|
+
unmerged_refs: list[str] = []
|
|
69
|
+
for full_ref, branch_oid in branch_oid_by_ref.items():
|
|
70
|
+
base_oid = base_oid_by_ref.get(full_ref)
|
|
71
|
+
if base_oid is None or branch_oid not in ancestor_oid_set_by_base_oid[base_oid]:
|
|
72
|
+
unmerged_refs.append(full_ref)
|
|
73
|
+
return tuple(unmerged_refs)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def commit_difference_in_memory(target_oid: str, head_ancestor_oid_set: set[str] | None, parent_oids_by_oid: dict[str, list[str]]) -> tuple[int, int] | None:
|
|
77
|
+
"""Count the commits only in HEAD and the commits only in the target, or None when memory cannot prove it.
|
|
78
|
+
|
|
79
|
+
The answer has the order of ``git rev-list --left-right --count HEAD...target``. None means the HEAD ancestry is
|
|
80
|
+
unknown (incomplete graph, or HEAD without a commit) or the target is not part of the loaded graph, so the caller
|
|
81
|
+
has to ask Git instead.
|
|
82
|
+
"""
|
|
83
|
+
if head_ancestor_oid_set is None or target_oid not in parent_oids_by_oid:
|
|
84
|
+
return None
|
|
85
|
+
target_ancestor_oid_set = ancestor_oid_set(target_oid, parent_oids_by_oid)
|
|
86
|
+
return len(head_ancestor_oid_set - target_ancestor_oid_set), len(target_ancestor_oid_set - head_ancestor_oid_set)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def commit_count_text(difference: tuple[int, int] | None) -> str:
|
|
90
|
+
"""Write a commit's position relative to HEAD the way Git Extensions does: "-13", "+2", "-3+2", "=" or nothing.
|
|
91
|
+
|
|
92
|
+
The first count is the commits HEAD has that the target lacks (shown with a minus), the second the commits the
|
|
93
|
+
target adds (shown with a plus). An unknown difference, such as HEAD without any commit, shows no text.
|
|
94
|
+
"""
|
|
95
|
+
if difference is None:
|
|
96
|
+
return ""
|
|
97
|
+
removed, added = difference
|
|
98
|
+
if removed == 0 and added == 0:
|
|
99
|
+
return SAME_COMMIT_TEXT
|
|
100
|
+
text = ""
|
|
101
|
+
if removed > 0:
|
|
102
|
+
text += REMOVED_COMMIT_SIGN + str(removed)
|
|
103
|
+
if added > 0:
|
|
104
|
+
text += ADDED_COMMIT_SIGN + str(added)
|
|
105
|
+
return text
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def revision_text_problem(text: str) -> str:
|
|
109
|
+
"""Say why a typed revision cannot be handed to Git at all, or answer an empty text when it may be.
|
|
110
|
+
|
|
111
|
+
This only guards the command line: a text that passes still has to be resolved, in memory or by Git.
|
|
112
|
+
"""
|
|
113
|
+
if not text:
|
|
114
|
+
return EMPTY_REVISION_CAUSE
|
|
115
|
+
if text.startswith(REVISION_OPTION_PREFIX):
|
|
116
|
+
return OPTION_REVISION_CAUSE
|
|
117
|
+
for character in text:
|
|
118
|
+
if character in COMMAND_LINE_CONTROL_CHARACTER_SET:
|
|
119
|
+
return CONTROL_CHARACTER_REVISION_CAUSE
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def reference_candidate_names(name: str) -> list[str]:
|
|
124
|
+
"""List the full reference names Git tries for a short name, in Git's own order of preference.
|
|
125
|
+
|
|
126
|
+
The order is the one of the gitrevisions documentation: the name itself, then under refs/, refs/tags/,
|
|
127
|
+
refs/heads/, refs/remotes/, and finally the default branch of a remote with that name.
|
|
128
|
+
"""
|
|
129
|
+
return [name, REFERENCE_ROOT_PREFIX + name, TAG_PREFIX + name, LOCAL_PREFIX + name, REMOTE_PREFIX + name,
|
|
130
|
+
REMOTE_PREFIX + name + REMOTE_DEFAULT_BRANCH_SUFFIX]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def resolve_revision_in_memory(text: str, references: tuple[Reference, ...], head_oid: str | None, parent_oids_by_oid: dict[str, list[str]]) -> str | None:
|
|
134
|
+
"""Resolve a typed revision to a full commit ID from what the window already holds, or None to ask Git.
|
|
135
|
+
|
|
136
|
+
Only answers Git would give the same way are taken: HEAD, a full commit ID already in the loaded graph, and a
|
|
137
|
+
reference name that exactly one reference matches under Git's lookup order. Everything else goes to Git: an
|
|
138
|
+
abbreviated ID (Git knows objects the window never loaded), a name several references match, a special name
|
|
139
|
+
kept beside HEAD, and any revision expression such as ``main~1`` or ``v1.0^2``.
|
|
140
|
+
"""
|
|
141
|
+
if text == HEAD_REVISION:
|
|
142
|
+
return head_oid
|
|
143
|
+
if FULL_OID_PATTERN.fullmatch(text) is not None:
|
|
144
|
+
return text if text in parent_oids_by_oid else None
|
|
145
|
+
if ROOT_REFERENCE_NAME_PATTERN.fullmatch(text) is not None:
|
|
146
|
+
return None
|
|
147
|
+
reference_by_name: dict[str, Reference] = {}
|
|
148
|
+
for reference in references:
|
|
149
|
+
reference_by_name[reference.full_name] = reference
|
|
150
|
+
matched_references: list[Reference] = []
|
|
151
|
+
for candidate_name in reference_candidate_names(text):
|
|
152
|
+
reference = reference_by_name.get(candidate_name)
|
|
153
|
+
if reference is not None:
|
|
154
|
+
matched_references.append(reference)
|
|
155
|
+
if len(matched_references) != 1:
|
|
156
|
+
return None
|
|
157
|
+
return matched_references[0].target_oid
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def complete_ancestor_oid_set(oid: str, parent_oids_by_oid: dict[str, list[str]], graph_complete: bool) -> set[str] | None:
|
|
162
|
+
"""Return a commit and all its ancestors from the graph in memory, or None when the graph cannot prove the whole set.
|
|
163
|
+
|
|
164
|
+
Only a completely read graph that holds the commit knows every one of its ancestors; a graph still being read, or
|
|
165
|
+
one that does not contain the commit, leaves the question to Git.
|
|
166
|
+
"""
|
|
167
|
+
if not graph_complete or oid not in parent_oids_by_oid:
|
|
168
|
+
return None
|
|
169
|
+
return ancestor_oid_set(oid, parent_oids_by_oid)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def is_ancestor_in_memory(ancestor_oid: str, descendant_ancestor_oid_set: set[str] | None) -> bool | None:
|
|
173
|
+
"""Tell whether a commit is an ancestor of (or equal to) a descendant whose ancestry is known, or None when it is not known.
|
|
174
|
+
|
|
175
|
+
This answers ``git merge-base --is-ancestor`` from memory: ``descendant_ancestor_oid_set`` is the descendant and
|
|
176
|
+
all its ancestors, as ``complete_ancestor_oid_set`` returns it, and None sends the question to Git.
|
|
177
|
+
"""
|
|
178
|
+
if descendant_ancestor_oid_set is None:
|
|
179
|
+
return None
|
|
180
|
+
return ancestor_oid in descendant_ancestor_oid_set
|