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,346 @@
|
|
|
1
|
+
"""Reproduce the Git Extensions "Create branch" window: a name, a revision, Checkout after create, the commit box, the Orphan group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QMargins, QRect, QSize, Qt, Signal
|
|
8
|
+
from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget
|
|
9
|
+
|
|
10
|
+
from gitpane.config import COMMIT_METADATA_UNAVAILABLE
|
|
11
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
12
|
+
from gitpane.reference.reference_edit import BranchIntent, BranchMode
|
|
13
|
+
from gitpane.text.typed_text import typed_value
|
|
14
|
+
from gitpane.ui.look.action_icon import action_icon
|
|
15
|
+
from gitpane.ui.look.theme_look import follow_theme
|
|
16
|
+
from gitpane.ui.look.windows_control_look import apply_windows_line_edit_look
|
|
17
|
+
from gitpane.ui.reference.commit_summary import GIT_EXTENSIONS_COMPACT_LAST_ROW_HEIGHT, CommitSummary, GitExtensionsGroupBox
|
|
18
|
+
from gitpane.ui.reference.reference_form import ReferenceFormDialog
|
|
19
|
+
from gitpane.ui.reference.revision_picker import RevisionPicker
|
|
20
|
+
from gitpane.ui.widget.dialog_layout import BandButtonSizing, fit_caption_column, fit_capture_control, install_banded_layout
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
|
|
26
|
+
from gitpane.history.repository_value import Commit
|
|
27
|
+
from gitpane.operation.repository_operation import OperationOutcome
|
|
28
|
+
from gitpane.reference.reference_snapshot import ReferenceSnapshot
|
|
29
|
+
|
|
30
|
+
CREATE_BRANCH_TITLE = "Create branch"
|
|
31
|
+
BRANCH_NAME_CAPTION = "Branch name"
|
|
32
|
+
CREATE_BRANCH_REVISION_CAPTION = "Create branch at this revision"
|
|
33
|
+
CHECKOUT_AFTER_CREATE_LABEL = "Checkout after create"
|
|
34
|
+
ORPHAN_GROUP_TITLE = "Orphan"
|
|
35
|
+
CREATE_ORPHAN_LABEL = "Create orphan"
|
|
36
|
+
CLEAR_ORPHAN_LABEL = "Clear working directory and index"
|
|
37
|
+
CREATE_BRANCH_BUTTON_TEXT = "Create branch"
|
|
38
|
+
# Client area of the Git Extensions capture (570 x 366 inside the window borders and title bar).
|
|
39
|
+
CREATE_BRANCH_CLIENT_SIZE = QSize(570, 366)
|
|
40
|
+
CREATE_BRANCH_BUTTON_SIZE = QSize(105, 23)
|
|
41
|
+
# Measured on the capture in client pixels at 100 %: white body y 0-324, band line y 325, band y 326-365; "Branch name"
|
|
42
|
+
# ink from x 18; name field (181, 15, 374, 23); "Create branch at this revision" ink from x 18; revision field
|
|
43
|
+
# (181, 45, 313, 23) with its choose button (498, 44, 23, 22) and the "-50" ink from x 531; Checkout after create square
|
|
44
|
+
# (181, 75, 13, 13); commit frame (14, 106, 542, 141); Orphan frame (15, 261, 540, 48) with its title ink from x 24,
|
|
45
|
+
# Create orphan square (34, 282, 13, 13) with its text from x 53, Clear working directory and index square
|
|
46
|
+
# (141, 282, 13, 13); Create branch button (451, 334, 105, 23) in the band.
|
|
47
|
+
CREATE_BRANCH_NAME_FIELD_WIDTH = 374
|
|
48
|
+
CREATE_BRANCH_REVISION_FIELD_WIDTH = 313
|
|
49
|
+
# The body starts at the commit frame's left edge and ends at its right edge, and its first control, the name field,
|
|
50
|
+
# sits 15 px from the top. The band sits at the bottom through a final stretch, so enlarging the resizable window only
|
|
51
|
+
# grows the empty body.
|
|
52
|
+
CREATE_BRANCH_BODY_MARGINS = QMargins(14, 15, 14, 0)
|
|
53
|
+
# The captions start 3 px right of the frame's edge (a label writes its text about 1 px after its own edge) and run to
|
|
54
|
+
# the fields at x 181. That width is a minimum: a caption wider in the system font widens the column and moves the
|
|
55
|
+
# fields right, so no caption is cut.
|
|
56
|
+
CREATE_BRANCH_CAPTION_INDENT = 3
|
|
57
|
+
CAPTION_WIDTH = 164
|
|
58
|
+
# Space between the name field (y 15-37) and the revision row, whose choose button is its highest control, at y 44.
|
|
59
|
+
NAME_TO_REVISION_SPACE = 6
|
|
60
|
+
# Space between the revision row (y 44-67) and the Checkout after create check box, whose square then lands on y 75.
|
|
61
|
+
REVISION_TO_CHECKOUT_SPACE = 4
|
|
62
|
+
# Space between the check box and the commit box, whose frame line then lands on y 106.
|
|
63
|
+
CHECKOUT_TO_FRAME_SPACE = 4
|
|
64
|
+
# The Orphan frame as painted, from the line crossing its title down to its bottom edge, in client pixels.
|
|
65
|
+
ORPHAN_GROUP_RECT = QRect(15, 261, 540, 48)
|
|
66
|
+
# First row below the commit box, whose frame's bottom edge is on y 246.
|
|
67
|
+
COMMIT_BOX_NEXT_ROW = 247
|
|
68
|
+
# Left edge of each check box square of the Orphan group, in client pixels; the style draws the square at the check
|
|
69
|
+
# box's own left edge, so Create orphan is exactly as wide as the gap up to Clear.
|
|
70
|
+
CREATE_ORPHAN_CHECK_LEFT = 34
|
|
71
|
+
CLEAR_ORPHAN_CHECK_LEFT = 141
|
|
72
|
+
# The capture only fixes Create orphan's width, the gap up to Clear; its height is its text's.
|
|
73
|
+
CREATE_ORPHAN_CAPTURE_SIZE = QSize(CLEAR_ORPHAN_CHECK_LEFT - CREATE_ORPHAN_CHECK_LEFT, 0)
|
|
74
|
+
# Distance from the Orphan frame line down to the check boxes, whose squares then land on y 282.
|
|
75
|
+
ORPHAN_LINE_TO_CHECK_SPACE = 18
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CreateBranchDialog(ReferenceFormDialog):
|
|
79
|
+
"""Create one branch at a commit, and optionally check it out, like the Git Extensions Create branch window.
|
|
80
|
+
|
|
81
|
+
The window only freezes a creation request; the reference form controller reads the references, fills the commit box from the
|
|
82
|
+
graph in memory, resolves typed revisions and runs Git.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
branch_requested = Signal(object)
|
|
86
|
+
|
|
87
|
+
def __init__(self, session_id: str, target_oid: str, parent: QWidget | None = None, *, clock: Callable[[], datetime] | None = None) -> None:
|
|
88
|
+
"""Build the name and revision rows, the check box, the commit box and the button band of the capture.
|
|
89
|
+
|
|
90
|
+
``clock`` dates the commit box, which reads the system time when it is absent.
|
|
91
|
+
"""
|
|
92
|
+
super().__init__(session_id, parent)
|
|
93
|
+
self.intent: BranchIntent | None = None
|
|
94
|
+
self.setWindowTitle(CREATE_BRANCH_TITLE)
|
|
95
|
+
# True once the commit box shows the target's subject, author and date; creation does not wait for it.
|
|
96
|
+
self.metadata_ready = False
|
|
97
|
+
# Set when Git could not read the target commit: a branch is never created on a commit nobody could show.
|
|
98
|
+
self.metadata_failed = False
|
|
99
|
+
layout = QVBoxLayout()
|
|
100
|
+
layout.setSpacing(0)
|
|
101
|
+
self.name_caption_label = self._caption(BRANCH_NAME_CAPTION)
|
|
102
|
+
self.name_edit = QLineEdit()
|
|
103
|
+
self.name_edit.setFixedWidth(CREATE_BRANCH_NAME_FIELD_WIDTH)
|
|
104
|
+
self.name_edit.setAccessibleName(BRANCH_NAME_CAPTION)
|
|
105
|
+
self.name_caption_label.setBuddy(self.name_edit)
|
|
106
|
+
name_row = self._row()
|
|
107
|
+
name_row.addWidget(self.name_caption_label, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
108
|
+
name_row.addWidget(self.name_edit)
|
|
109
|
+
name_row.addStretch(1)
|
|
110
|
+
layout.addLayout(name_row)
|
|
111
|
+
layout.addSpacing(NAME_TO_REVISION_SPACE)
|
|
112
|
+
self.revision_caption_label = self._caption(CREATE_BRANCH_REVISION_CAPTION)
|
|
113
|
+
self.picker = RevisionPicker(CREATE_BRANCH_REVISION_FIELD_WIDTH)
|
|
114
|
+
self.revision_caption_label.setBuddy(self.picker.revision_edit)
|
|
115
|
+
revision_row = self._row()
|
|
116
|
+
revision_row.addWidget(self.revision_caption_label, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
117
|
+
revision_row.addWidget(self.picker, 1)
|
|
118
|
+
layout.addLayout(revision_row)
|
|
119
|
+
layout.addSpacing(REVISION_TO_CHECKOUT_SPACE)
|
|
120
|
+
self.checkout_checkbox = QCheckBox(CHECKOUT_AFTER_CREATE_LABEL)
|
|
121
|
+
self.checkout_checkbox.setChecked(True)
|
|
122
|
+
# Indented by the caption column, whose width follows the captions' text.
|
|
123
|
+
self.checkout_row = QHBoxLayout()
|
|
124
|
+
self.checkout_row.setSpacing(0)
|
|
125
|
+
self.checkout_row.addWidget(self.checkout_checkbox)
|
|
126
|
+
self.checkout_row.addStretch(1)
|
|
127
|
+
layout.addLayout(self.checkout_row)
|
|
128
|
+
layout.addSpacing(CHECKOUT_TO_FRAME_SPACE)
|
|
129
|
+
# The v6.0 commit box: its last row is as high as the others and long values are cut instead of wrapped.
|
|
130
|
+
self.commit_summary = CommitSummary(target_oid, git_extensions_look=True, last_row_height=GIT_EXTENSIONS_COMPACT_LAST_ROW_HEIGHT, single_line=True, clock=clock)
|
|
131
|
+
layout.addWidget(self.commit_summary)
|
|
132
|
+
self.orphan_group = GitExtensionsGroupBox(ORPHAN_GROUP_TITLE)
|
|
133
|
+
self.orphan_checkbox = QCheckBox(CREATE_ORPHAN_LABEL)
|
|
134
|
+
# Checked by default like Git Extensions, and only usable once Create orphan is checked.
|
|
135
|
+
self.clear_checkbox = QCheckBox(CLEAR_ORPHAN_LABEL)
|
|
136
|
+
self.clear_checkbox.setChecked(True)
|
|
137
|
+
self._lay_out_orphan_group(layout)
|
|
138
|
+
layout.addStretch(1)
|
|
139
|
+
self.submit_button.setText(CREATE_BRANCH_BUTTON_TEXT)
|
|
140
|
+
self.submit_button.setIcon(action_icon("branch_create", self.devicePixelRatioF()))
|
|
141
|
+
self.submit_button.setEnabled(False)
|
|
142
|
+
# Git Extensions shows this form as a white body over a grey band holding its single button; there is no
|
|
143
|
+
# Cancel button, Escape and the title bar close it.
|
|
144
|
+
self.apply_theme_look()
|
|
145
|
+
self.button_band = install_banded_layout(self, layout, [self.submit_button], body_margins=CREATE_BRANCH_BODY_MARGINS, button_sizing=BandButtonSizing.WINDOW)
|
|
146
|
+
follow_theme(self, self.apply_theme_look)
|
|
147
|
+
# Enter runs Create branch from anywhere in the form. The role is set only now, once the button belongs to the
|
|
148
|
+
# dialog: Qt then records it as the dialog's main default and gives it back whenever the focus moves.
|
|
149
|
+
self.submit_button.setDefault(True)
|
|
150
|
+
self.submit_button.clicked.connect(self._submit)
|
|
151
|
+
self.name_edit.textChanged.connect(self._update_intent)
|
|
152
|
+
self.checkout_checkbox.toggled.connect(self._update_intent)
|
|
153
|
+
self.orphan_checkbox.toggled.connect(self._orphan_toggled)
|
|
154
|
+
self.clear_checkbox.toggled.connect(self._update_intent)
|
|
155
|
+
self._apply_orphan_rules()
|
|
156
|
+
# Connected before the reference form controller binds the field, so a new target's position is invalidated and its commit
|
|
157
|
+
# box reset before they are filled again.
|
|
158
|
+
self.picker.target_changed.connect(self._target_changed)
|
|
159
|
+
self.resize(CREATE_BRANCH_CLIENT_SIZE)
|
|
160
|
+
self.refresh_metrics()
|
|
161
|
+
self.name_edit.setFocus()
|
|
162
|
+
|
|
163
|
+
def apply_theme_look(self) -> None:
|
|
164
|
+
"""Lay the light or dark look of the name field and the revision picker, decided on the application palette; called again at each switch.
|
|
165
|
+
|
|
166
|
+
The body and the band follow the switch on their own. Only style sheets and palettes change: the typed name,
|
|
167
|
+
the revision and the check boxes stay as they are.
|
|
168
|
+
"""
|
|
169
|
+
apply_windows_line_edit_look(self.name_edit)
|
|
170
|
+
self.picker.apply_theme_look()
|
|
171
|
+
|
|
172
|
+
def _lay_out_orphan_group(self, layout: QVBoxLayout) -> None:
|
|
173
|
+
"""Place the Orphan frame and its two check boxes where the capture has them, below the commit box.
|
|
174
|
+
|
|
175
|
+
The frame line crosses the middle of the title, a style-dependent distance below the group's top edge; that
|
|
176
|
+
distance is read from the group itself so the painted line, not the widget edge, lands on the measured row.
|
|
177
|
+
"""
|
|
178
|
+
line_offset = self.orphan_group.frame_rectangle().top()
|
|
179
|
+
self.orphan_group.setFixedHeight(line_offset + ORPHAN_GROUP_RECT.height())
|
|
180
|
+
# A layout inside a group box starts at the style's content area, below the native title and inside the
|
|
181
|
+
# native frame, neither of which this painted frame has. The two check boxes are therefore placed at their
|
|
182
|
+
# measured positions inside the group instead of through a layout.
|
|
183
|
+
check_top = line_offset + ORPHAN_LINE_TO_CHECK_SPACE
|
|
184
|
+
self.orphan_checkbox.setParent(self.orphan_group)
|
|
185
|
+
self.orphan_checkbox.move(CREATE_ORPHAN_CHECK_LEFT - ORPHAN_GROUP_RECT.left(), check_top)
|
|
186
|
+
self.clear_checkbox.setParent(self.orphan_group)
|
|
187
|
+
self.clear_checkbox.move(CLEAR_ORPHAN_CHECK_LEFT - ORPHAN_GROUP_RECT.left(), check_top)
|
|
188
|
+
layout.addSpacing(ORPHAN_GROUP_RECT.top() - line_offset - COMMIT_BOX_NEXT_ROW)
|
|
189
|
+
# The Orphan frame is 1 px narrower than the commit box on each side.
|
|
190
|
+
group_row = QHBoxLayout()
|
|
191
|
+
group_row.setContentsMargins(ORPHAN_GROUP_RECT.left() - CREATE_BRANCH_BODY_MARGINS.left(), 0,
|
|
192
|
+
CREATE_BRANCH_CLIENT_SIZE.width() - CREATE_BRANCH_BODY_MARGINS.right() - ORPHAN_GROUP_RECT.right() - 1, 0)
|
|
193
|
+
group_row.addWidget(self.orphan_group)
|
|
194
|
+
layout.addLayout(group_row)
|
|
195
|
+
|
|
196
|
+
def fit_capture_controls(self) -> None:
|
|
197
|
+
"""Give the button, the caption column and the Orphan check boxes the capture's sizes, or more when their text needs it.
|
|
198
|
+
|
|
199
|
+
With the captures' font everything keeps the capture's size and place. A wider font widens the caption column,
|
|
200
|
+
which moves both fields and the Checkout after create check box right by the same amount, widens the Create
|
|
201
|
+
branch button, and pushes Clear working directory and index right of a wider Create orphan; the window then
|
|
202
|
+
grows to its content when it is shown.
|
|
203
|
+
"""
|
|
204
|
+
fit_capture_control(self.submit_button, CREATE_BRANCH_BUTTON_SIZE)
|
|
205
|
+
caption_width = fit_caption_column((self.name_caption_label, self.revision_caption_label), CAPTION_WIDTH)
|
|
206
|
+
self.checkout_row.setContentsMargins(CREATE_BRANCH_CAPTION_INDENT + caption_width, 0, 0, 0)
|
|
207
|
+
fit_capture_control(self.orphan_checkbox, CREATE_ORPHAN_CAPTURE_SIZE)
|
|
208
|
+
self.clear_checkbox.adjustSize()
|
|
209
|
+
self.clear_checkbox.move(self.orphan_checkbox.geometry().right() + 1, self.orphan_checkbox.y())
|
|
210
|
+
# Clear keeps the same distance from the frame's right edge as Create orphan has from its left edge, at least.
|
|
211
|
+
self.orphan_group.setMinimumWidth(self.clear_checkbox.geometry().right() + 1 + self.orphan_checkbox.x())
|
|
212
|
+
|
|
213
|
+
def _caption(self, text: str) -> QLabel:
|
|
214
|
+
"""Build a caption of the left column; its width is set with the column's by ``fit_capture_controls``."""
|
|
215
|
+
return QLabel(text)
|
|
216
|
+
|
|
217
|
+
def _row(self) -> QHBoxLayout:
|
|
218
|
+
"""Start a caption row indented from the body edge like the captured captions."""
|
|
219
|
+
row = QHBoxLayout()
|
|
220
|
+
row.setContentsMargins(CREATE_BRANCH_CAPTION_INDENT, 0, 0, 0)
|
|
221
|
+
row.setSpacing(0)
|
|
222
|
+
return row
|
|
223
|
+
|
|
224
|
+
def populate(self, snapshot: ReferenceSnapshot, target_oid: str, name: str) -> None:
|
|
225
|
+
"""Show the commit and the proposed name against the references the window was opened on.
|
|
226
|
+
|
|
227
|
+
The name is selected and focused, as Git Extensions does, so typing replaces it. A name the user already
|
|
228
|
+
typed while the references were being checked is kept.
|
|
229
|
+
"""
|
|
230
|
+
if self.closed or self.busy or self.completed:
|
|
231
|
+
return
|
|
232
|
+
self.snapshot = snapshot
|
|
233
|
+
self.commit_summary.show_references(snapshot)
|
|
234
|
+
if not typed_value(self.name_edit.text()):
|
|
235
|
+
self.name_edit.setText(name)
|
|
236
|
+
self.name_edit.selectAll()
|
|
237
|
+
self.name_edit.setFocus()
|
|
238
|
+
self.set_target(target_oid)
|
|
239
|
+
|
|
240
|
+
def set_target(self, oid: str, *, rewrite_text: bool = True) -> None:
|
|
241
|
+
"""Target another commit; its short hash replaces the field unless the user's typed text is kept."""
|
|
242
|
+
if self.closed or self.busy or self.completed:
|
|
243
|
+
return
|
|
244
|
+
self.picker.show_revision(oid, rewrite_text=rewrite_text)
|
|
245
|
+
|
|
246
|
+
def _target_changed(self, oid: str | None) -> None:
|
|
247
|
+
"""Drop the previous target's position and commit details, then freeze the new request, if any."""
|
|
248
|
+
self.position_generation += 1
|
|
249
|
+
if oid is not None and oid != self.commit_summary.target_oid:
|
|
250
|
+
self.commit_summary.retarget(oid)
|
|
251
|
+
self.metadata_ready = False
|
|
252
|
+
self.metadata_failed = False
|
|
253
|
+
if self.snapshot is not None:
|
|
254
|
+
self.commit_summary.show_references(self.snapshot)
|
|
255
|
+
self._update_intent()
|
|
256
|
+
|
|
257
|
+
def show_loaded_commit(self, commit: Commit) -> None:
|
|
258
|
+
"""Fill the commit box from a commit the graph already holds, without asking Git."""
|
|
259
|
+
if self.closed or commit.oid != self.commit_summary.target_oid:
|
|
260
|
+
return
|
|
261
|
+
self.commit_summary.show_loaded_commit(commit)
|
|
262
|
+
self.metadata_ready = True
|
|
263
|
+
|
|
264
|
+
def show_metadata(self, fields: tuple[str, ...] | None, cause: str = "") -> None:
|
|
265
|
+
"""Fill the commit box from Git's answer, or refuse the target Git could not read and explain why."""
|
|
266
|
+
if self.closed:
|
|
267
|
+
return
|
|
268
|
+
if fields is None:
|
|
269
|
+
self.metadata_ready = False
|
|
270
|
+
self.metadata_failed = True
|
|
271
|
+
self.commit_summary.subject_label.setText(COMMIT_METADATA_UNAVAILABLE)
|
|
272
|
+
self._update_intent()
|
|
273
|
+
self.show_error(cause)
|
|
274
|
+
return
|
|
275
|
+
oid_field_index = 0
|
|
276
|
+
if fields[oid_field_index] != self.commit_summary.target_oid:
|
|
277
|
+
return
|
|
278
|
+
self.commit_summary.show_commit(fields)
|
|
279
|
+
self.metadata_ready = True
|
|
280
|
+
|
|
281
|
+
def _orphan_toggled(self) -> None:
|
|
282
|
+
"""Apply the Git Extensions rules of the Orphan group, then freeze the new request."""
|
|
283
|
+
self._apply_orphan_rules()
|
|
284
|
+
self._update_intent()
|
|
285
|
+
|
|
286
|
+
def _apply_orphan_rules(self) -> None:
|
|
287
|
+
"""Follow Git Extensions: an orphan branch is always checked out, and only an orphan branch can clear the working directory.
|
|
288
|
+
|
|
289
|
+
Checking Create orphan checks and locks Checkout after create and frees Clear; unchecking it frees Checkout after
|
|
290
|
+
create and locks Clear, both keeping their state.
|
|
291
|
+
"""
|
|
292
|
+
orphan = self.orphan_checkbox.isChecked()
|
|
293
|
+
usable = self.orphan_checkbox.isEnabledTo(self)
|
|
294
|
+
if orphan:
|
|
295
|
+
self.checkout_checkbox.setChecked(True)
|
|
296
|
+
self.checkout_checkbox.setEnabled(usable and not orphan)
|
|
297
|
+
self.clear_checkbox.setEnabled(usable and orphan)
|
|
298
|
+
|
|
299
|
+
def _update_intent(self) -> None:
|
|
300
|
+
"""Freeze the typed name, the target commit and the checkout and orphan choices into the request Create branch would send.
|
|
301
|
+
|
|
302
|
+
The name is taken without the blanks around it; the field keeps what was typed.
|
|
303
|
+
"""
|
|
304
|
+
self.intent = None
|
|
305
|
+
target_oid = self.picker.target_oid
|
|
306
|
+
name = typed_value(self.name_edit.text())
|
|
307
|
+
orphan = self.orphan_checkbox.isChecked()
|
|
308
|
+
if self.snapshot is not None and target_oid and name and not self.metadata_failed:
|
|
309
|
+
try:
|
|
310
|
+
self.intent = BranchIntent.capture(self.snapshot, target_oid, session_id=self.session_id, name=name,
|
|
311
|
+
checkout=self.checkout_checkbox.isChecked(), mode=BranchMode.CREATE, orphan=orphan,
|
|
312
|
+
clear=orphan and self.clear_checkbox.isChecked())
|
|
313
|
+
except ValueError:
|
|
314
|
+
self.intent = None
|
|
315
|
+
self.submit_button.setEnabled(self.intent is not None and not self.busy and not self.completed and not self.closed)
|
|
316
|
+
|
|
317
|
+
def _submit(self) -> None:
|
|
318
|
+
"""Freeze the form and hand its request to the reference form controller once."""
|
|
319
|
+
if self.closed or self.busy or self.completed or self.intent is None or not self.submit_button.isEnabled():
|
|
320
|
+
return
|
|
321
|
+
self.busy = True
|
|
322
|
+
self._set_controls_enabled(False)
|
|
323
|
+
self.branch_requested.emit(self.intent)
|
|
324
|
+
|
|
325
|
+
def show_outcome(self, outcome: OperationOutcome) -> None:
|
|
326
|
+
"""Close after a successful creation; keep the form after any other result while the console explains it.
|
|
327
|
+
|
|
328
|
+
A refused or failed creation wrote nothing, so the name stays editable for another try; after a partial or
|
|
329
|
+
unknown result the branch may exist, so the form only shows what was asked.
|
|
330
|
+
"""
|
|
331
|
+
self.busy = False
|
|
332
|
+
if outcome.status == OperationStatus.SUCCESS:
|
|
333
|
+
self.accept()
|
|
334
|
+
return
|
|
335
|
+
if outcome.status in (OperationStatus.REFUSED, OperationStatus.FAILED):
|
|
336
|
+
self._set_controls_enabled(True)
|
|
337
|
+
self._update_intent()
|
|
338
|
+
return
|
|
339
|
+
self.completed = True
|
|
340
|
+
self._set_controls_enabled(False)
|
|
341
|
+
|
|
342
|
+
def _set_controls_enabled(self, enabled: bool) -> None:
|
|
343
|
+
"""Enable or disable every choice of the form together, keeping the check boxes the Orphan group locks."""
|
|
344
|
+
for widget in (self.name_edit, self.picker, self.checkout_checkbox, self.orphan_checkbox, self.clear_checkbox, self.submit_button):
|
|
345
|
+
widget.setEnabled(enabled)
|
|
346
|
+
self._apply_orphan_rules()
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Reproduce the Git Extensions "Create tag" window: a name, a revision, Push tag to '<remote>', the tag type, its message."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QMargins, QPoint, QRect, QSize, Qt, Signal
|
|
8
|
+
from PySide6.QtWidgets import QComboBox, QHBoxLayout, QLabel, QLineEdit, QPlainTextEdit, QVBoxLayout, QWidget
|
|
9
|
+
|
|
10
|
+
from gitpane.operation.operation_status import OperationStatus
|
|
11
|
+
from gitpane.reference.reference_edit import DEFAULT_PUSH_REMOTE_NAME, TagIntent
|
|
12
|
+
from gitpane.text.typed_text import typed_value
|
|
13
|
+
from gitpane.ui.look.action_icon import action_icon
|
|
14
|
+
from gitpane.ui.look.theme_look import follow_theme
|
|
15
|
+
from gitpane.ui.look.windows_control_look import apply_windows_button_look, apply_windows_combo_look, apply_windows_line_edit_look, apply_windows_message_look
|
|
16
|
+
from gitpane.ui.reference.reference_form import ReferenceFormDialog
|
|
17
|
+
from gitpane.ui.reference.revision_picker import RevisionPicker
|
|
18
|
+
from gitpane.ui.widget.dialog_layout import fit_caption_column, fit_capture_control, install_plain_form_palette
|
|
19
|
+
from gitpane.ui.widget.long_text import ElidedCheckBox
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from gitpane.operation.repository_operation import OperationOutcome
|
|
23
|
+
from gitpane.reference.reference_snapshot import ReferenceSnapshot
|
|
24
|
+
|
|
25
|
+
CREATE_TAG_TITLE = "Create tag"
|
|
26
|
+
TAG_NAME_CAPTION = "Tag name"
|
|
27
|
+
CREATE_TAG_REVISION_CAPTION = "Create tag at this revision"
|
|
28
|
+
# Git Extensions writes the remote the tag would be pushed to in the check box itself.
|
|
29
|
+
PUSH_TAG_CAPTION = "Push tag to '{0}'"
|
|
30
|
+
# The two tag types Git Extensions offers without a GPG key; the signed variants are deliberately not offered.
|
|
31
|
+
TAG_TYPE_LABELS = ("Lightweight tag", "Annotated tag")
|
|
32
|
+
LIGHTWEIGHT_TAG_INDEX = 0
|
|
33
|
+
ANNOTATED_TAG_INDEX = 1
|
|
34
|
+
MESSAGE_CAPTION = "Message"
|
|
35
|
+
CREATE_TAG_BUTTON_TEXT = "Create tag"
|
|
36
|
+
# Client area of the Git Extensions window (its designer's ClientSize, 464 x 245 at 100 %).
|
|
37
|
+
CREATE_TAG_CLIENT_SIZE = QSize(464, 245)
|
|
38
|
+
CREATE_TAG_SUBMIT_SIZE = QSize(126, 24)
|
|
39
|
+
# Measured on the capture in client pixels at 100 %, visible borders included: captions ink from x 14; name field
|
|
40
|
+
# (157, 11, 296, 23); revision field (157, 41, 235, 23) with its choose button (396, 40, 23, 22); Push square at
|
|
41
|
+
# (157, 71); tag type list (157, 95, 150, 23); Create tag button (326, 209, 126, 24), 12 px from the right and bottom
|
|
42
|
+
# edges. The capture's Key Id row is not reproduced, so the message area and its caption move up by that row's 28 px
|
|
43
|
+
# and the area grows by as much: (156, 122, 298, 81) instead of (156, 150, 298, 53).
|
|
44
|
+
CREATE_TAG_NAME_FIELD_RECT = QRect(157, 11, 296, 23)
|
|
45
|
+
CREATE_TAG_REVISION_FIELD_RECT = QRect(157, 41, 235, 23)
|
|
46
|
+
CREATE_TAG_CHOOSE_BUTTON_RECT = QRect(396, 40, 23, 22)
|
|
47
|
+
CREATE_TAG_TYPE_COMBO_RECT = QRect(157, 95, 150, 23)
|
|
48
|
+
CREATE_TAG_SUBMIT_RECT = QRect(QPoint(326, 209), CREATE_TAG_SUBMIT_SIZE)
|
|
49
|
+
MESSAGE_AREA_RECT = QRect(156, 122, 298, 81)
|
|
50
|
+
# Qt writes a caption from the label's own left edge, so the labels start where the captured captions' ink does.
|
|
51
|
+
CREATE_TAG_CAPTION_LEFT = 14
|
|
52
|
+
# Width of the caption column, from the captions' left edge to the fields; a wider caption text widens it.
|
|
53
|
+
CREATE_TAG_CAPTION_WIDTH = CREATE_TAG_NAME_FIELD_RECT.left() - CREATE_TAG_CAPTION_LEFT
|
|
54
|
+
# The message area starts this much left of the fields above it, as on the capture.
|
|
55
|
+
MESSAGE_AREA_LEFT_SHIFT = CREATE_TAG_NAME_FIELD_RECT.left() - MESSAGE_AREA_RECT.left()
|
|
56
|
+
# Space between the revision row (y 40-63) and the Push check box, whose square then lands on y 71.
|
|
57
|
+
REVISION_TO_PUSH_SPACE = 4
|
|
58
|
+
# Space between the Push check box, 21 px high in the Fusion style, and the tag type list at y 95.
|
|
59
|
+
PUSH_TO_TYPE_SPACE = 6
|
|
60
|
+
# The window's own margins: the name field and the message area set the top and right ones, the button the bottom one.
|
|
61
|
+
CREATE_TAG_MARGINS = QMargins(CREATE_TAG_CAPTION_LEFT, CREATE_TAG_NAME_FIELD_RECT.top(), CREATE_TAG_CLIENT_SIZE.width() - MESSAGE_AREA_RECT.right() - 1,
|
|
62
|
+
CREATE_TAG_CLIENT_SIZE.height() - CREATE_TAG_SUBMIT_RECT.bottom() - 1)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CreateTagDialog(ReferenceFormDialog):
|
|
66
|
+
"""Create one tag at a commit, lightweight or annotated, like the Git Extensions Create tag window.
|
|
67
|
+
|
|
68
|
+
The window only freezes a creation request; the reference form controller reads the references, resolves typed revisions and
|
|
69
|
+
runs Git. Its Push check box never pushes: after creation the reference form controller opens the Push window on the new tag.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
tag_requested = Signal(object)
|
|
73
|
+
|
|
74
|
+
def __init__(self, session_id: str, target_oid: str, parent: QWidget | None = None) -> None:
|
|
75
|
+
"""Build the rows of the capture, show the requested commit in the revision field and focus the name."""
|
|
76
|
+
super().__init__(session_id, parent)
|
|
77
|
+
self.intent: TagIntent | None = None
|
|
78
|
+
self.setWindowTitle(CREATE_TAG_TITLE)
|
|
79
|
+
# The remote the check box names; the reference form controller replaces it with the current branch's upstream remote.
|
|
80
|
+
self.push_remote = DEFAULT_PUSH_REMOTE_NAME
|
|
81
|
+
# A plain grey window without a button band, with the resize grip of the capture in its bottom right corner.
|
|
82
|
+
install_plain_form_palette(self)
|
|
83
|
+
self.setSizeGripEnabled(True)
|
|
84
|
+
layout = QVBoxLayout(self)
|
|
85
|
+
layout.setContentsMargins(CREATE_TAG_MARGINS)
|
|
86
|
+
layout.setSpacing(0)
|
|
87
|
+
self.name_edit = QLineEdit()
|
|
88
|
+
self.name_edit.setAccessibleName(TAG_NAME_CAPTION)
|
|
89
|
+
self.name_caption_label = self._caption(TAG_NAME_CAPTION, self.name_edit)
|
|
90
|
+
# The name field ends 1 px before the message area, whose right edge sets the window's right margin.
|
|
91
|
+
name_row = self._row(right_margin=MESSAGE_AREA_RECT.right() - CREATE_TAG_NAME_FIELD_RECT.right())
|
|
92
|
+
name_row.addWidget(self.name_caption_label, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
93
|
+
name_row.addWidget(self.name_edit, 1)
|
|
94
|
+
layout.addLayout(name_row)
|
|
95
|
+
# The revision row's highest control is its choose button.
|
|
96
|
+
layout.addSpacing(CREATE_TAG_CHOOSE_BUTTON_RECT.top() - CREATE_TAG_NAME_FIELD_RECT.bottom() - 1)
|
|
97
|
+
self.picker = RevisionPicker(CREATE_TAG_REVISION_FIELD_RECT.width())
|
|
98
|
+
self.revision_caption_label = self._caption(CREATE_TAG_REVISION_CAPTION, self.picker.revision_edit)
|
|
99
|
+
revision_row = self._row()
|
|
100
|
+
revision_row.addWidget(self.revision_caption_label, 0, Qt.AlignmentFlag.AlignVCenter)
|
|
101
|
+
revision_row.addWidget(self.picker, 1)
|
|
102
|
+
layout.addLayout(revision_row)
|
|
103
|
+
layout.addSpacing(REVISION_TO_PUSH_SPACE)
|
|
104
|
+
# A long remote name is cut with an ellipsis instead of widening the window; the tooltip then gives it whole.
|
|
105
|
+
self.push_checkbox = ElidedCheckBox(PUSH_TAG_CAPTION.format(self.push_remote))
|
|
106
|
+
# The rows without caption are indented to the fields by fit_capture_controls, which follows the caption column.
|
|
107
|
+
self.push_row = self._row()
|
|
108
|
+
self.push_row.addWidget(self.push_checkbox, 1)
|
|
109
|
+
layout.addLayout(self.push_row)
|
|
110
|
+
layout.addSpacing(PUSH_TO_TYPE_SPACE)
|
|
111
|
+
self.tag_type_combo = QComboBox()
|
|
112
|
+
self.tag_type_combo.setEditable(False)
|
|
113
|
+
self.tag_type_combo.addItems(TAG_TYPE_LABELS)
|
|
114
|
+
self.tag_type_combo.setCurrentIndex(LIGHTWEIGHT_TAG_INDEX)
|
|
115
|
+
self.tag_type_combo.setFixedWidth(CREATE_TAG_TYPE_COMBO_RECT.width())
|
|
116
|
+
self.tag_type_combo.setAccessibleName("Tag type")
|
|
117
|
+
self.type_row = self._row()
|
|
118
|
+
self.type_row.addWidget(self.tag_type_combo)
|
|
119
|
+
self.type_row.addStretch(1)
|
|
120
|
+
layout.addLayout(self.type_row)
|
|
121
|
+
layout.addSpacing(MESSAGE_AREA_RECT.top() - CREATE_TAG_TYPE_COMBO_RECT.bottom() - 1)
|
|
122
|
+
self.message_edit = QPlainTextEdit()
|
|
123
|
+
self.message_edit.setAccessibleName(MESSAGE_CAPTION)
|
|
124
|
+
# Tab moves on to the button instead of being typed into the annotation, as in the Git Extensions text box.
|
|
125
|
+
self.message_edit.setTabChangesFocus(True)
|
|
126
|
+
self.message_caption_label = self._caption(MESSAGE_CAPTION, self.message_edit)
|
|
127
|
+
message_row = self._row()
|
|
128
|
+
message_row.addWidget(self.message_caption_label, 0, Qt.AlignmentFlag.AlignTop)
|
|
129
|
+
message_row.addWidget(self.message_edit, 1)
|
|
130
|
+
# The message area takes every pixel the window gains in height, as the full-height row of Git Extensions does.
|
|
131
|
+
layout.addLayout(message_row, 1)
|
|
132
|
+
layout.addSpacing(CREATE_TAG_SUBMIT_RECT.top() - MESSAGE_AREA_RECT.bottom() - 1)
|
|
133
|
+
self.submit_button.setText(CREATE_TAG_BUTTON_TEXT)
|
|
134
|
+
self.submit_button.setIcon(action_icon("tag_create", self.devicePixelRatioF()))
|
|
135
|
+
self.submit_button.setEnabled(False)
|
|
136
|
+
# There is no Cancel button, as in Git Extensions: Escape and the title bar close the window.
|
|
137
|
+
button_row = self._row(right_margin=MESSAGE_AREA_RECT.right() - CREATE_TAG_SUBMIT_RECT.right())
|
|
138
|
+
button_row.addStretch(1)
|
|
139
|
+
button_row.addWidget(self.submit_button)
|
|
140
|
+
layout.addLayout(button_row)
|
|
141
|
+
self.apply_theme_look()
|
|
142
|
+
follow_theme(self, self.apply_theme_look)
|
|
143
|
+
# Enter runs Create tag from the single-line fields. The role is set once the button belongs to the dialog, so
|
|
144
|
+
# Qt records it as the dialog's main default; the message area keeps Enter for its new lines.
|
|
145
|
+
self.submit_button.setDefault(True)
|
|
146
|
+
self.submit_button.clicked.connect(self._submit)
|
|
147
|
+
self.name_edit.textChanged.connect(self._update_intent)
|
|
148
|
+
self.message_edit.textChanged.connect(self._update_intent)
|
|
149
|
+
self.push_checkbox.toggled.connect(self._update_intent)
|
|
150
|
+
self.tag_type_combo.currentIndexChanged.connect(self._tag_type_changed)
|
|
151
|
+
self._apply_tag_type()
|
|
152
|
+
# Connected before the reference form controller binds the field, so a new target's position is invalidated before it is counted.
|
|
153
|
+
self.picker.target_changed.connect(self._target_changed)
|
|
154
|
+
self.picker.show_revision(target_oid)
|
|
155
|
+
self.resize(CREATE_TAG_CLIENT_SIZE)
|
|
156
|
+
self.refresh_metrics()
|
|
157
|
+
self.name_edit.setFocus()
|
|
158
|
+
|
|
159
|
+
def apply_theme_look(self) -> None:
|
|
160
|
+
"""Lay the light or dark look of the fields, the tag type list, the message area and the button, decided on the application palette.
|
|
161
|
+
|
|
162
|
+
Called by the constructor and again at each switch. The message area takes the look of its current state:
|
|
163
|
+
bordered when an annotated tag lets it be typed in, hidden in the window when it is disabled. Only style sheets
|
|
164
|
+
and palettes change: the name, the revision, the tag type, the message and Push stay as they are.
|
|
165
|
+
"""
|
|
166
|
+
apply_windows_line_edit_look(self.name_edit)
|
|
167
|
+
apply_windows_combo_look(self.tag_type_combo, editable=False)
|
|
168
|
+
apply_windows_message_look(self.message_edit)
|
|
169
|
+
apply_windows_button_look(self.submit_button)
|
|
170
|
+
self.picker.apply_theme_look()
|
|
171
|
+
|
|
172
|
+
def fit_capture_controls(self) -> None:
|
|
173
|
+
"""Give the caption column and the Create tag button the capture's sizes, or more when their text needs it.
|
|
174
|
+
|
|
175
|
+
A wider caption column moves every field right by the same amount: the name and revision fields, the Push check
|
|
176
|
+
box, the tag type list, and the message area, which stays 1 px left of the fields as on the capture.
|
|
177
|
+
"""
|
|
178
|
+
column_width = fit_caption_column((self.name_caption_label, self.revision_caption_label), CREATE_TAG_CAPTION_WIDTH)
|
|
179
|
+
fit_caption_column((self.message_caption_label,), column_width - MESSAGE_AREA_LEFT_SHIFT)
|
|
180
|
+
for row in (self.push_row, self.type_row):
|
|
181
|
+
row.setContentsMargins(column_width, 0, 0, 0)
|
|
182
|
+
fit_capture_control(self.submit_button, CREATE_TAG_SUBMIT_SIZE)
|
|
183
|
+
|
|
184
|
+
def _caption(self, text: str, buddy: QWidget) -> QLabel:
|
|
185
|
+
"""Build a caption of the left column whose mnemonic and accessibility point at its field; ``fit_capture_controls`` sets its width."""
|
|
186
|
+
label = QLabel(text)
|
|
187
|
+
label.setBuddy(buddy)
|
|
188
|
+
return label
|
|
189
|
+
|
|
190
|
+
def _row(self, right_margin: int = 0) -> QHBoxLayout:
|
|
191
|
+
"""Start a row of the window, flush with the captions on the left."""
|
|
192
|
+
row = QHBoxLayout()
|
|
193
|
+
row.setContentsMargins(0, 0, right_margin, 0)
|
|
194
|
+
row.setSpacing(0)
|
|
195
|
+
return row
|
|
196
|
+
|
|
197
|
+
def populate(self, snapshot: ReferenceSnapshot, target_oid: str, push_remote: str) -> None:
|
|
198
|
+
"""Show the commit and the push remote against the references the window was opened on.
|
|
199
|
+
|
|
200
|
+
A name or message the user already typed while the references were being checked is kept.
|
|
201
|
+
"""
|
|
202
|
+
if self.closed or self.busy or self.completed:
|
|
203
|
+
return
|
|
204
|
+
self.snapshot = snapshot
|
|
205
|
+
self.push_remote = push_remote
|
|
206
|
+
self.push_checkbox.setText(PUSH_TAG_CAPTION.format(push_remote))
|
|
207
|
+
self.set_target(target_oid)
|
|
208
|
+
self.name_edit.setFocus()
|
|
209
|
+
|
|
210
|
+
def set_target(self, oid: str, *, rewrite_text: bool = True) -> None:
|
|
211
|
+
"""Target another commit; its short hash replaces the field unless the user's typed text is kept."""
|
|
212
|
+
if self.closed or self.busy or self.completed:
|
|
213
|
+
return
|
|
214
|
+
self.picker.show_revision(oid, rewrite_text=rewrite_text)
|
|
215
|
+
|
|
216
|
+
def _target_changed(self, _oid: str | None) -> None:
|
|
217
|
+
"""Drop any position still being counted for the previous target and freeze the new request, if any."""
|
|
218
|
+
self.position_generation += 1
|
|
219
|
+
self._update_intent()
|
|
220
|
+
|
|
221
|
+
def _tag_type_changed(self) -> None:
|
|
222
|
+
"""Follow the chosen tag type in the message area, then freeze the new request."""
|
|
223
|
+
self._apply_tag_type()
|
|
224
|
+
self._update_intent()
|
|
225
|
+
|
|
226
|
+
def _apply_tag_type(self) -> None:
|
|
227
|
+
"""Offer the message area, with its border, only for an annotated tag, as Git Extensions does.
|
|
228
|
+
|
|
229
|
+
A lightweight tag has no message: the area is disabled and loses its border, and what was typed in it stays
|
|
230
|
+
there, unused, until the tag is annotated again.
|
|
231
|
+
"""
|
|
232
|
+
annotated = self.tag_type_combo.currentIndex() == ANNOTATED_TAG_INDEX
|
|
233
|
+
editable = not self.busy and not self.completed and not self.closed
|
|
234
|
+
self.message_edit.setEnabled(annotated and editable)
|
|
235
|
+
apply_windows_message_look(self.message_edit)
|
|
236
|
+
|
|
237
|
+
def _update_intent(self) -> None:
|
|
238
|
+
"""Freeze the typed name, the target commit, the tag type, its message and the Push choice into the request.
|
|
239
|
+
|
|
240
|
+
The name and the message are taken without the blanks around them; the fields keep what was typed.
|
|
241
|
+
"""
|
|
242
|
+
self.intent = None
|
|
243
|
+
target_oid = self.picker.target_oid
|
|
244
|
+
name = typed_value(self.name_edit.text())
|
|
245
|
+
if self.snapshot is not None and target_oid and name:
|
|
246
|
+
try:
|
|
247
|
+
self.intent = TagIntent.capture(self.snapshot, target_oid, session_id=self.session_id, name=name,
|
|
248
|
+
annotated=self.tag_type_combo.currentIndex() == ANNOTATED_TAG_INDEX,
|
|
249
|
+
message=typed_value(self.message_edit.toPlainText()), push_after_creation=self.push_checkbox.isChecked())
|
|
250
|
+
except ValueError:
|
|
251
|
+
self.intent = None
|
|
252
|
+
self.submit_button.setEnabled(self.intent is not None and not self.busy and not self.completed and not self.closed)
|
|
253
|
+
|
|
254
|
+
def _submit(self) -> None:
|
|
255
|
+
"""Freeze the form and hand its request to the reference form controller once."""
|
|
256
|
+
if self.closed or self.busy or self.completed or self.intent is None or not self.submit_button.isEnabled():
|
|
257
|
+
return
|
|
258
|
+
self.busy = True
|
|
259
|
+
self._set_controls_enabled(False)
|
|
260
|
+
self.tag_requested.emit(self.intent)
|
|
261
|
+
|
|
262
|
+
def show_outcome(self, outcome: OperationOutcome) -> None:
|
|
263
|
+
"""Close after a successful creation; keep the form after any other result while the console explains it.
|
|
264
|
+
|
|
265
|
+
A refused or failed creation wrote nothing, so the form stays editable for another try; after a partial or
|
|
266
|
+
unknown result the tag may exist, so the form only shows what was asked.
|
|
267
|
+
"""
|
|
268
|
+
self.busy = False
|
|
269
|
+
if outcome.status == OperationStatus.SUCCESS:
|
|
270
|
+
self.accept()
|
|
271
|
+
return
|
|
272
|
+
if outcome.status in (OperationStatus.REFUSED, OperationStatus.FAILED):
|
|
273
|
+
self._set_controls_enabled(True)
|
|
274
|
+
self._update_intent()
|
|
275
|
+
return
|
|
276
|
+
self.completed = True
|
|
277
|
+
self._set_controls_enabled(False)
|
|
278
|
+
|
|
279
|
+
def _set_controls_enabled(self, enabled: bool) -> None:
|
|
280
|
+
"""Enable or disable every choice of the form together, keeping the message area for annotated tags only."""
|
|
281
|
+
for widget in (self.name_edit, self.picker, self.push_checkbox, self.tag_type_combo, self.submit_button):
|
|
282
|
+
widget.setEnabled(enabled)
|
|
283
|
+
self._apply_tag_type()
|