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.
Files changed (353) hide show
  1. gitpane/__init__.py +0 -0
  2. gitpane/account/__init__.py +0 -0
  3. gitpane/account/account_choice.py +297 -0
  4. gitpane/account/address_owner.py +67 -0
  5. gitpane/account/authentication.py +1811 -0
  6. gitpane/account/authentication_worker.py +836 -0
  7. gitpane/account/credential_manager.py +480 -0
  8. gitpane/account/credential_manager_install.py +171 -0
  9. gitpane/account/keyring_reader.py +135 -0
  10. gitpane/account/keyring_state.py +158 -0
  11. gitpane/account/ssh.py +312 -0
  12. gitpane/account/ssh_askpass.py +216 -0
  13. gitpane/account/ssh_prompt.py +222 -0
  14. gitpane/application.py +684 -0
  15. gitpane/commit/__init__.py +0 -0
  16. gitpane/commit/commit_metadata.py +88 -0
  17. gitpane/commit/commit_push.py +276 -0
  18. gitpane/commit/commit_write.py +830 -0
  19. gitpane/config.py +1066 -0
  20. gitpane/controller/__init__.py +0 -0
  21. gitpane/controller/account_controller.py +677 -0
  22. gitpane/controller/clone_controller.py +776 -0
  23. gitpane/controller/close_readiness.py +212 -0
  24. gitpane/controller/commit_timing_controller.py +566 -0
  25. gitpane/controller/conflict_controller.py +885 -0
  26. gitpane/controller/console_registry.py +38 -0
  27. gitpane/controller/emoji_window_controller.py +95 -0
  28. gitpane/controller/file_action_controller.py +479 -0
  29. gitpane/controller/history_read_controller.py +671 -0
  30. gitpane/controller/history_rewrite_controller.py +739 -0
  31. gitpane/controller/navigation_controller.py +658 -0
  32. gitpane/controller/opening_controller.py +375 -0
  33. gitpane/controller/operation_policy.py +76 -0
  34. gitpane/controller/rebase_controller.py +797 -0
  35. gitpane/controller/rebase_editor_controller.py +309 -0
  36. gitpane/controller/reference_form_controller.py +1677 -0
  37. gitpane/controller/reference_read_controller.py +242 -0
  38. gitpane/controller/refresh_controller.py +477 -0
  39. gitpane/controller/replay_controller.py +805 -0
  40. gitpane/controller/session_registry.py +337 -0
  41. gitpane/controller/setting_controller.py +592 -0
  42. gitpane/controller/shutdown_controller.py +345 -0
  43. gitpane/controller/staging_controller.py +199 -0
  44. gitpane/controller/staging_session_registry.py +47 -0
  45. gitpane/controller/stash_controller.py +367 -0
  46. gitpane/controller/transport_controller.py +813 -0
  47. gitpane/controller/welcome_controller.py +102 -0
  48. gitpane/controller/window_activation.py +116 -0
  49. gitpane/controller/worktree_state_controller.py +169 -0
  50. gitpane/emoji/__init__.py +0 -0
  51. gitpane/emoji/emoji_catalogue.py +156 -0
  52. gitpane/git_config/__init__.py +0 -0
  53. gitpane/git_config/git_setting.py +441 -0
  54. gitpane/history/__init__.py +0 -0
  55. gitpane/history/avatar.py +241 -0
  56. gitpane/history/file_history.py +359 -0
  57. gitpane/history/graph.py +145 -0
  58. gitpane/history/history_filter.py +31 -0
  59. gitpane/history/history_parse.py +605 -0
  60. gitpane/history/repository.py +1600 -0
  61. gitpane/history/repository_value.py +341 -0
  62. gitpane/history/revision.py +180 -0
  63. gitpane/history/revision_file.py +182 -0
  64. gitpane/history_rewrite/__init__.py +0 -0
  65. gitpane/history_rewrite/graph.py +210 -0
  66. gitpane/history_rewrite/guard.py +288 -0
  67. gitpane/history_rewrite/index.py +125 -0
  68. gitpane/history_rewrite/prepare.py +378 -0
  69. gitpane/history_rewrite/protocol.py +320 -0
  70. gitpane/history_rewrite/publish.py +444 -0
  71. gitpane/history_rewrite/recovery.py +481 -0
  72. gitpane/history_rewrite/value.py +229 -0
  73. gitpane/operation/__init__.py +0 -0
  74. gitpane/operation/console_line.py +313 -0
  75. gitpane/operation/console_tone.py +76 -0
  76. gitpane/operation/diagnostic.py +954 -0
  77. gitpane/operation/launch_signature.py +254 -0
  78. gitpane/operation/operation_status.py +87 -0
  79. gitpane/operation/operation_trace.py +766 -0
  80. gitpane/operation/repository_operation.py +781 -0
  81. gitpane/preference/__init__.py +0 -0
  82. gitpane/preference/preference_location.py +40 -0
  83. gitpane/preference/preference_store.py +590 -0
  84. gitpane/process/__init__.py +0 -0
  85. gitpane/process/exact_arguments.py +77 -0
  86. gitpane/process/git_output_text.py +20 -0
  87. gitpane/process/process_group.py +851 -0
  88. gitpane/process/process_log.py +114 -0
  89. gitpane/process/process_runner.py +1033 -0
  90. gitpane/process/released_child_watch.py +138 -0
  91. gitpane/recent/__init__.py +0 -0
  92. gitpane/recent/mount_table.py +422 -0
  93. gitpane/recent/recent_availability.py +414 -0
  94. gitpane/recent/recent_name.py +11 -0
  95. gitpane/recent/recent_search.py +114 -0
  96. gitpane/reference/__init__.py +0 -0
  97. gitpane/reference/reference_edit.py +2030 -0
  98. gitpane/reference/reference_snapshot.py +196 -0
  99. gitpane/reference/stash.py +730 -0
  100. gitpane/reference/stash_list.py +164 -0
  101. gitpane/sequencer/__init__.py +0 -0
  102. gitpane/sequencer/commit_message_rule.py +154 -0
  103. gitpane/sequencer/conflict.py +506 -0
  104. gitpane/sequencer/merge_recovery.py +163 -0
  105. gitpane/sequencer/merge_state.py +166 -0
  106. gitpane/sequencer/mergetool.py +856 -0
  107. gitpane/sequencer/rebase.py +750 -0
  108. gitpane/sequencer/rebase_edit_file.py +224 -0
  109. gitpane/sequencer/rebase_state.py +523 -0
  110. gitpane/sequencer/replay.py +1435 -0
  111. gitpane/sequencer/replay_legacy.py +253 -0
  112. gitpane/sequencer/replay_state.py +747 -0
  113. gitpane/submodule/__init__.py +0 -0
  114. gitpane/submodule/checkout_refusal.py +102 -0
  115. gitpane/submodule/checkout_refusal_notice.py +111 -0
  116. gitpane/submodule/cloned_without_checkout.py +65 -0
  117. gitpane/submodule/submodule_discovery.py +1180 -0
  118. gitpane/submodule/submodule_loss.py +218 -0
  119. gitpane/system/__init__.py +0 -0
  120. gitpane/system/appearance_portal.py +125 -0
  121. gitpane/system/application_identity.py +48 -0
  122. gitpane/system/command_line.py +36 -0
  123. gitpane/system/desktop_icon_fact.py +128 -0
  124. gitpane/system/desktop_icon_support.py +270 -0
  125. gitpane/system/desktop_shortcut.py +536 -0
  126. gitpane/system/icns_image.py +35 -0
  127. gitpane/system/installed_launcher.py +136 -0
  128. gitpane/system/linux_distribution.py +151 -0
  129. gitpane/system/network_folder.py +46 -0
  130. gitpane/system/path_identity.py +95 -0
  131. gitpane/system/private_tree_removal.py +47 -0
  132. gitpane/system/process_program.py +63 -0
  133. gitpane/system/qt_folder.py +38 -0
  134. gitpane/system/ready_channel_watch.py +67 -0
  135. gitpane/system/startup_activation.py +65 -0
  136. gitpane/system/terminal_detach.py +384 -0
  137. gitpane/system/xdg_session.py +85 -0
  138. gitpane/text/__init__.py +0 -0
  139. gitpane/text/text_query.py +97 -0
  140. gitpane/text/typed_path.py +75 -0
  141. gitpane/text/typed_text.py +17 -0
  142. gitpane/thread/__init__.py +0 -0
  143. gitpane/thread/weak_signal.py +93 -0
  144. gitpane/tool/__init__.py +0 -0
  145. gitpane/tool/app_bundle.py +164 -0
  146. gitpane/tool/desktop_text_editor.py +156 -0
  147. gitpane/tool/difftool.py +429 -0
  148. gitpane/tool/editor.py +445 -0
  149. gitpane/tool/editor_bridge.py +193 -0
  150. gitpane/tool/editor_recipe.py +481 -0
  151. gitpane/tool/file_reveal.py +94 -0
  152. gitpane/tool/git_executable.py +264 -0
  153. gitpane/tool/git_version.py +62 -0
  154. gitpane/tool/kdiff3.py +41 -0
  155. gitpane/tool/kdiff3_install.py +162 -0
  156. gitpane/tool/launch_services_text_editor.py +104 -0
  157. gitpane/tool/mergetool_recipe.py +84 -0
  158. gitpane/tool/system_editor_default.py +80 -0
  159. gitpane/tool/system_text_editor.py +44 -0
  160. gitpane/tool/system_tool.py +816 -0
  161. gitpane/tool/terminal_recipe.py +390 -0
  162. gitpane/transport/__init__.py +0 -0
  163. gitpane/transport/branch_exchange.py +1690 -0
  164. gitpane/transport/clone.py +1273 -0
  165. gitpane/transport/credential_read_retry.py +35 -0
  166. gitpane/transport/git_refusal.py +207 -0
  167. gitpane/transport/push_tracking.py +121 -0
  168. gitpane/transport/refusal_notice.py +225 -0
  169. gitpane/transport/remote.py +747 -0
  170. gitpane/transport/remote_config.py +544 -0
  171. gitpane/ui/__init__.py +0 -0
  172. gitpane/ui/console/__init__.py +0 -0
  173. gitpane/ui/console/error_dialog.py +332 -0
  174. gitpane/ui/console/operation_dialog.py +408 -0
  175. gitpane/ui/console/operation_output.py +91 -0
  176. gitpane/ui/console/operation_output_view.py +373 -0
  177. gitpane/ui/emoji/__init__.py +0 -0
  178. gitpane/ui/emoji/emoji_font.py +257 -0
  179. gitpane/ui/emoji/emoji_grid.py +125 -0
  180. gitpane/ui/emoji/emoji_window.py +154 -0
  181. gitpane/ui/emoji/file_action_icon.py +131 -0
  182. gitpane/ui/emoji/opentype_family_name.py +182 -0
  183. gitpane/ui/history_rewrite/__init__.py +0 -0
  184. gitpane/ui/history_rewrite/details.py +78 -0
  185. gitpane/ui/history_rewrite/preview.py +565 -0
  186. gitpane/ui/history_rewrite/preview_model.py +291 -0
  187. gitpane/ui/history_rewrite/question.py +87 -0
  188. gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
  189. gitpane/ui/look/__init__.py +0 -0
  190. gitpane/ui/look/action_icon.py +504 -0
  191. gitpane/ui/look/application_icon.py +78 -0
  192. gitpane/ui/look/application_style.py +317 -0
  193. gitpane/ui/look/avatar_label.py +53 -0
  194. gitpane/ui/look/color_contrast.py +43 -0
  195. gitpane/ui/look/desktop_appearance_follower.py +118 -0
  196. gitpane/ui/look/fixed_font.py +21 -0
  197. gitpane/ui/look/gtk_settings.py +116 -0
  198. gitpane/ui/look/icon_density.py +16 -0
  199. gitpane/ui/look/reference_font.py +19 -0
  200. gitpane/ui/look/theme_color.py +63 -0
  201. gitpane/ui/look/theme_look.py +95 -0
  202. gitpane/ui/look/theme_palette.py +173 -0
  203. gitpane/ui/look/warning_icon.py +86 -0
  204. gitpane/ui/look/windows_control_look.py +250 -0
  205. gitpane/ui/reader/__init__.py +0 -0
  206. gitpane/ui/reader/commit_message.py +129 -0
  207. gitpane/ui/reader/commit_panel.py +378 -0
  208. gitpane/ui/reader/diff_panel.py +626 -0
  209. gitpane/ui/reader/file_action.py +138 -0
  210. gitpane/ui/reader/file_tree_panel.py +235 -0
  211. gitpane/ui/reader/reader_toolbar.py +56 -0
  212. gitpane/ui/reader/revision_tree.py +80 -0
  213. gitpane/ui/reader/search_result.py +165 -0
  214. gitpane/ui/reader/source_text.py +134 -0
  215. gitpane/ui/reader/source_view.py +151 -0
  216. gitpane/ui/reader/syntax_span.py +67 -0
  217. gitpane/ui/reference/__init__.py +0 -0
  218. gitpane/ui/reference/branch_selector.py +106 -0
  219. gitpane/ui/reference/checkout_dialog.py +419 -0
  220. gitpane/ui/reference/checkout_revision_dialog.py +182 -0
  221. gitpane/ui/reference/commit_summary.py +339 -0
  222. gitpane/ui/reference/create_branch_dialog.py +346 -0
  223. gitpane/ui/reference/create_tag_dialog.py +283 -0
  224. gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
  225. gitpane/ui/reference/delete_tag_dialog.py +394 -0
  226. gitpane/ui/reference/form_opening.py +53 -0
  227. gitpane/ui/reference/reference_dialog.py +417 -0
  228. gitpane/ui/reference/reference_feedback.py +62 -0
  229. gitpane/ui/reference/reference_form.py +139 -0
  230. gitpane/ui/reference/rename_branch_dialog.py +179 -0
  231. gitpane/ui/reference/reset_branch_dialog.py +430 -0
  232. gitpane/ui/reference/reset_dialog.py +263 -0
  233. gitpane/ui/reference/revision_dialog.py +121 -0
  234. gitpane/ui/reference/revision_picker.py +227 -0
  235. gitpane/ui/reference/select_branches_dialog.py +199 -0
  236. gitpane/ui/reference/stash_dialog.py +171 -0
  237. gitpane/ui/repository/__init__.py +0 -0
  238. gitpane/ui/repository/graph_delegate.py +298 -0
  239. gitpane/ui/repository/history_avatar.py +130 -0
  240. gitpane/ui/repository/history_model.py +179 -0
  241. gitpane/ui/repository/repository_list_popup.py +319 -0
  242. gitpane/ui/repository/repository_window.py +2580 -0
  243. gitpane/ui/resource/ArrowDown.png +0 -0
  244. gitpane/ui/resource/ArrowUp.png +0 -0
  245. gitpane/ui/resource/BranchCheckout.png +0 -0
  246. gitpane/ui/resource/BranchCreate.png +0 -0
  247. gitpane/ui/resource/BranchDelete.png +0 -0
  248. gitpane/ui/resource/CopyToClipboard.png +0 -0
  249. gitpane/ui/resource/GitPane-16.png +0 -0
  250. gitpane/ui/resource/GitPane-24.png +0 -0
  251. gitpane/ui/resource/GitPane-256.png +0 -0
  252. gitpane/ui/resource/GitPane-32.png +0 -0
  253. gitpane/ui/resource/GitPane-48.png +0 -0
  254. gitpane/ui/resource/GitPane-64.png +0 -0
  255. gitpane/ui/resource/GitPane.ico +0 -0
  256. gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  257. gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
  258. gitpane/ui/resource/Renamed.png +0 -0
  259. gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
  260. gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
  261. gitpane/ui/resource/Stash.png +0 -0
  262. gitpane/ui/resource/Tag.png +0 -0
  263. gitpane/ui/resource/TagCreate.png +0 -0
  264. gitpane/ui/resource/TagDelete.png +0 -0
  265. gitpane/ui/resource/Unmerged.png +0 -0
  266. gitpane/ui/resource/checkout.png +0 -0
  267. gitpane/ui/sequencer/__init__.py +0 -0
  268. gitpane/ui/sequencer/conflict_dialog.py +532 -0
  269. gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
  270. gitpane/ui/sequencer/rebase_dialog.py +461 -0
  271. gitpane/ui/sequencer/rebase_editor_window.py +220 -0
  272. gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
  273. gitpane/ui/sequencer/replay_dialog.py +501 -0
  274. gitpane/ui/setting/__init__.py +0 -0
  275. gitpane/ui/setting/account_page.py +724 -0
  276. gitpane/ui/setting/add_account_dialog.py +1005 -0
  277. gitpane/ui/setting/credential_manager_dialog.py +151 -0
  278. gitpane/ui/setting/git_setting_widget.py +388 -0
  279. gitpane/ui/setting/setting_block.py +19 -0
  280. gitpane/ui/setting/setting_dialog.py +750 -0
  281. gitpane/ui/setting/ssh_widget.py +232 -0
  282. gitpane/ui/setting/utility_page.py +240 -0
  283. gitpane/ui/staging/__init__.py +0 -0
  284. gitpane/ui/staging/staging_action.py +356 -0
  285. gitpane/ui/staging/staging_window.py +745 -0
  286. gitpane/ui/transport/__init__.py +0 -0
  287. gitpane/ui/transport/clone_dialog.py +879 -0
  288. gitpane/ui/transport/push_dialog.py +580 -0
  289. gitpane/ui/transport/push_rejection_prompt.py +100 -0
  290. gitpane/ui/transport/push_tracking_question.py +88 -0
  291. gitpane/ui/transport/remote_dialog.py +943 -0
  292. gitpane/ui/welcome/__init__.py +0 -0
  293. gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
  294. gitpane/ui/welcome/welcome_window.py +1137 -0
  295. gitpane/ui/widget/__init__.py +0 -0
  296. gitpane/ui/widget/busy_indicator.py +106 -0
  297. gitpane/ui/widget/commit_date_text.py +55 -0
  298. gitpane/ui/widget/content_growth.py +262 -0
  299. gitpane/ui/widget/content_measure.py +99 -0
  300. gitpane/ui/widget/dialog_layout.py +668 -0
  301. gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
  302. gitpane/ui/widget/flow_layout.py +110 -0
  303. gitpane/ui/widget/long_text.py +542 -0
  304. gitpane/ui/widget/mnemonic.py +13 -0
  305. gitpane/ui/widget/numbered_text_edit.py +127 -0
  306. gitpane/ui/widget/plain_label.py +15 -0
  307. gitpane/ui/widget/plain_tooltip.py +19 -0
  308. gitpane/ui/widget/refresh_advice.py +25 -0
  309. gitpane/ui/widget/result_label.py +227 -0
  310. gitpane/ui/widget/result_status_bar.py +53 -0
  311. gitpane/ui/widget/scrolled_text.py +69 -0
  312. gitpane/ui/widget/shortcut_text.py +31 -0
  313. gitpane/ui/widget/standard_icon_label.py +55 -0
  314. gitpane/ui/widget/system_shortcut.py +75 -0
  315. gitpane/ui/widget/unavailable_reason.py +29 -0
  316. gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
  317. gitpane/ui/window_control/__init__.py +0 -0
  318. gitpane/ui/window_control/button_focus.py +42 -0
  319. gitpane/ui/window_control/compositor_answer.py +152 -0
  320. gitpane/ui/window_control/compositor_growth_cap.py +73 -0
  321. gitpane/ui/window_control/desktop_activation.py +203 -0
  322. gitpane/ui/window_control/dialog_modality.py +122 -0
  323. gitpane/ui/window_control/item_activation.py +42 -0
  324. gitpane/ui/window_control/modal_focus_relay.py +124 -0
  325. gitpane/ui/window_control/wayland_activation.py +695 -0
  326. gitpane/ui/window_control/window_placement.py +305 -0
  327. gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  328. gitpane/vendor/w32.exe +0 -0
  329. gitpane/vendor/w64-arm.exe +0 -0
  330. gitpane/vendor/w64.exe +0 -0
  331. gitpane/worktree/__init__.py +0 -0
  332. gitpane/worktree/command_sequence.py +110 -0
  333. gitpane/worktree/file_action_target.py +61 -0
  334. gitpane/worktree/file_menu_action.py +52 -0
  335. gitpane/worktree/historical_restore.py +361 -0
  336. gitpane/worktree/ignore_rule.py +266 -0
  337. gitpane/worktree/mutation.py +1539 -0
  338. gitpane/worktree/partial_patch.py +435 -0
  339. gitpane/worktree/staged_reset.py +447 -0
  340. gitpane/worktree/staging.py +1418 -0
  341. gitpane/worktree/trash.py +302 -0
  342. gitpane/worktree/worktree_file.py +406 -0
  343. gitpane/worktree/worktree_service.py +584 -0
  344. gitpane/worktree/worktree_snapshot.py +630 -0
  345. gitpane-1.0.0.dist-info/METADATA +141 -0
  346. gitpane-1.0.0.dist-info/RECORD +353 -0
  347. gitpane-1.0.0.dist-info/WHEEL +5 -0
  348. gitpane-1.0.0.dist-info/entry_points.txt +2 -0
  349. gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
  350. gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
  351. gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  352. gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  353. gitpane-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,339 @@
1
+ """Present a compact, read-only summary of a captured revision."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import QEvent, QPoint, QRect, Qt
9
+ from PySide6.QtGui import QColor, QPainter, QPalette
10
+ from PySide6.QtWidgets import QApplication, QGridLayout, QGroupBox, QLabel, QStyle, QStyleOptionGroupBox
11
+
12
+ from gitpane.history.history_parse import COMMIT_TIME_UNAVAILABLE_CAUSE
13
+ from gitpane.history.repository_value import ReferenceKind
14
+ from gitpane.ui.look.theme_color import is_dark_palette, theme_line_color
15
+ from gitpane.ui.look.theme_palette import WINDOWS_CONTROL_LIGHT_COLOR
16
+ from gitpane.ui.widget.commit_date_text import DATE_UNAVAILABLE_TEXT, format_commit_date
17
+ from gitpane.ui.widget.dialog_layout import CAPTION_TO_FIELD_MIN_SPACE
18
+ from gitpane.ui.widget.long_text import ElidedLabel, WrappingLabel
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Callable
22
+
23
+ from PySide6.QtGui import QPaintEvent
24
+
25
+ from gitpane.history.repository_value import Commit
26
+ from gitpane.reference.reference_snapshot import ReferenceSnapshot
27
+
28
+ REVISION_PREFIX_LENGTH = 7
29
+ # Git Extensions writes eight hexadecimal digits in its commit box.
30
+ GIT_EXTENSIONS_REVISION_PREFIX_LENGTH = 8
31
+ LOADING_COMMIT_TEXT = "Loading commit metadata…"
32
+ NOT_AVAILABLE_TEXT = "n/a"
33
+ # Qt centres a label one pixel lower in a 21 px row than Windows Forms does; a one-pixel bottom margin lifts it.
34
+ GIT_EXTENSIONS_TEXT_LIFT = 1
35
+ # Git Extensions colors, in a light theme: frame line and empty reference band sampled from its Cherry pick commit
36
+ # capture, branch band LightSalmon (also sampled), tag band LightSteelBlue as its source code sets it.
37
+ FRAME_LIGHT_COLOR = "#dcdcdc"
38
+ BRANCH_BAND_LIGHT_COLOR = "#ffa07a"
39
+ TAG_BAND_LIGHT_COLOR = "#b0c4de"
40
+ EMPTY_REFERENCE_BAND_LIGHT_COLOR = WINDOWS_CONTROL_LIGHT_COLOR
41
+ # Dark counterparts of the salmon and steel blue bands: same hues, dark enough for light text (contrast above 7:1).
42
+ BRANCH_BAND_DARK_COLOR = "#6e3524"
43
+ TAG_BAND_DARK_COLOR = "#34465e"
44
+ # Space kept free on each side of the short hash where it interrupts the frame line.
45
+ TITLE_LINE_GAP = 1
46
+ # Git Extensions starts the short hash 9 px after the frame corner, leaving a short piece of line on its left.
47
+ TITLE_INDENT = 9
48
+ # Its digits sit slightly above the line's middle; Qt centres them slightly below, hence this small lift.
49
+ TITLE_TEXT_RAISE = 2
50
+ # Inside the Git Extensions commit box (measured on its capture, from the frame's left, right and bottom edges and from
51
+ # the frame line): subject 18 px under the line and 15 px in, 13 px of space before the metadata rows, rows 21 px high
52
+ # except the last one (31 px), captions 17 px in, values 102 px in, bands ending 13 px before the right edge, 10 px of
53
+ # space under the last row.
54
+ GIT_EXTENSIONS_CONTENT_LEFT = 15
55
+ GIT_EXTENSIONS_CONTENT_TOP_BELOW_LINE = 18
56
+ GIT_EXTENSIONS_CONTENT_RIGHT = 13
57
+ GIT_EXTENSIONS_CONTENT_BOTTOM = 10
58
+ GIT_EXTENSIONS_SUBJECT_TO_METADATA_SPACE = 13
59
+ GIT_EXTENSIONS_METADATA_ROW_HEIGHT = 21
60
+ GIT_EXTENSIONS_LAST_ROW_HEIGHT = 31
61
+ # The Git Extensions v6.0 windows (Create branch, Reset branch) give their last row the height of the others, so their
62
+ # frame is 141 px high instead of the 151 px of Revert and Cherry-pick.
63
+ GIT_EXTENSIONS_COMPACT_LAST_ROW_HEIGHT = GIT_EXTENSIONS_METADATA_ROW_HEIGHT
64
+ GIT_EXTENSIONS_CAPTION_INDENT = 2
65
+ GIT_EXTENSIONS_CAPTION_COLUMN_WIDTH = 87
66
+ # The colored band starts this far left of the value text.
67
+ VALUE_BAND_LEAD = 2
68
+
69
+
70
+ class GitExtensionsGroupBox(QGroupBox):
71
+ """Group frame drawn like the Git Extensions ones: a thin grey rectangle whose caption interrupts its top line.
72
+
73
+ The caption starts a few pixels after the frame's corner, leaving a short piece of line on its left. The frame is
74
+ painted here rather than through a style sheet: a style sheet on the frame would also restyle every child and
75
+ change the native metrics the layout was measured with, while painting only replaces the frame drawing.
76
+ """
77
+
78
+ def frame_color(self) -> QColor:
79
+ """Use the captured frame grey in a light theme and the theme's line grey in a dark one.
80
+
81
+ The dark grey is worked out from the application palette at paint time, like every other line of the window, so
82
+ the frame shows on the dark body and follows a theme change.
83
+ """
84
+ if is_dark_palette(self.palette()):
85
+ return theme_line_color(QApplication.palette())
86
+ return QColor(FRAME_LIGHT_COLOR)
87
+
88
+ def title_rectangle(self) -> QRect:
89
+ """Return where the caption is written: at the style's caption height, moved off the corner."""
90
+ option = QStyleOptionGroupBox()
91
+ self.initStyleOption(option)
92
+ style_rectangle = self.style().subControlRect(QStyle.ComplexControl.CC_GroupBox, option, QStyle.SubControl.SC_GroupBoxLabel, self)
93
+ return QRect(TITLE_INDENT, style_rectangle.top(), self.fontMetrics().horizontalAdvance(self.title()), style_rectangle.height())
94
+
95
+ def frame_rectangle(self) -> QRect:
96
+ """Return the painted frame: from the line crossing the caption down to the bottom edge."""
97
+ return QRect(QPoint(0, self.title_rectangle().center().y()), QPoint(self.width() - 1, self.height() - 1))
98
+
99
+ def paintEvent(self, event: QPaintEvent) -> None:
100
+ """Draw the frame, clear its line behind the caption and write the caption across it."""
101
+ title_rect = self.title_rectangle()
102
+ frame = self.frame_rectangle()
103
+ painter = QPainter(self)
104
+ painter.setPen(self.frame_color())
105
+ painter.drawRect(QRect(frame.left(), frame.top(), frame.width() - 1, frame.height() - 1))
106
+ painter.fillRect(title_rect.adjusted(-TITLE_LINE_GAP, 0, TITLE_LINE_GAP, 0), self.palette().color(QPalette.ColorRole.Window))
107
+ painter.setPen(self.palette().color(QPalette.ColorRole.WindowText))
108
+ painter.drawText(title_rect.translated(0, -TITLE_TEXT_RAISE), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self.title())
109
+ painter.end()
110
+
111
+
112
+ class CommitSummary(GitExtensionsGroupBox):
113
+ """Keep a revision's subject and essential metadata aligned in a small form.
114
+
115
+ With ``git_extensions_look`` the frame reproduces the Git Extensions commit box: the short hash is written across
116
+ the frame's top line, and the Branch(es) and Tag(s) values sit on colored bands. Other forms keep the native frame.
117
+ ``last_row_height`` sets the height of the Tag(s) row of that look, and ``single_line`` cuts long values with an
118
+ ellipsis instead of wrapping them, so the box never grows the window. ``clock`` gives the current time the relative
119
+ commit date is written against; it is injected so that date can be reproduced, as the window gallery needs, and
120
+ reads the system time when absent.
121
+ """
122
+
123
+ def __init__(self, target_oid: str, *, git_extensions_look: bool = False, last_row_height: int = GIT_EXTENSIONS_LAST_ROW_HEIGHT,
124
+ single_line: bool = False, clock: Callable[[], datetime] | None = None) -> None:
125
+ """Retain the complete target while using a short hash as the frame caption."""
126
+ super().__init__(target_oid[:REVISION_PREFIX_LENGTH])
127
+ self.target_oid = target_oid
128
+ self.clock = clock or (lambda: datetime.now(UTC))
129
+ self.git_extensions_look = git_extensions_look
130
+ self.last_row_height = last_row_height
131
+ self.single_line = single_line
132
+ self.has_branch = False
133
+ self.has_tag = False
134
+ self.setToolTip(target_oid)
135
+ self.setAccessibleName("Target revision " + target_oid)
136
+ self.grid = QGridLayout(self)
137
+ self.subject_label = self._label(LOADING_COMMIT_TEXT, bold=True)
138
+ self.author_label = self._label(NOT_AVAILABLE_TEXT, bold=True)
139
+ self.date_label = self._label(NOT_AVAILABLE_TEXT)
140
+ self.branch_label = self._label(NOT_AVAILABLE_TEXT)
141
+ self.tag_label = self._label(NOT_AVAILABLE_TEXT)
142
+ subject_row, spacing_row = 0, 1
143
+ # The Git Extensions look keeps an empty row between the subject and the metadata instead of the grid spacing.
144
+ first_metadata_row = spacing_row + 1 if git_extensions_look else spacing_row
145
+ caption_column, value_column, column_count = 0, 1, 2
146
+ self.grid.addWidget(self.subject_label, subject_row, caption_column, 1, column_count)
147
+ self.caption_labels: list[QLabel] = []
148
+ # Git Extensions centres each caption on its value row; the native form keeps it on the value's first line.
149
+ caption_alignment = Qt.AlignmentFlag(0) if git_extensions_look else Qt.AlignmentFlag.AlignTop
150
+ for row, (caption, label) in enumerate((("Author:", self.author_label), ("Commit date:", self.date_label),
151
+ ("Branch(es):", self.branch_label), ("Tag(s):", self.tag_label)), start=first_metadata_row):
152
+ caption_label = QLabel(caption)
153
+ self.caption_labels.append(caption_label)
154
+ self.grid.addWidget(caption_label, row, caption_column, alignment=caption_alignment)
155
+ label.setAccessibleName(caption.removesuffix(":"))
156
+ self.grid.addWidget(label, row, value_column)
157
+ self.grid.setColumnStretch(value_column, 1)
158
+ if git_extensions_look:
159
+ self._use_git_extensions_metrics(spacing_row, first_metadata_row)
160
+
161
+ def _use_git_extensions_metrics(self, spacing_row: int, first_metadata_row: int) -> None:
162
+ """Space the subject, captions and values like the Git Extensions commit box instead of the native form."""
163
+ self.grid.setHorizontalSpacing(0)
164
+ self.grid.setVerticalSpacing(0)
165
+ self.grid.setRowMinimumHeight(spacing_row, GIT_EXTENSIONS_SUBJECT_TO_METADATA_SPACE)
166
+ metadata_row_count = len(self.caption_labels)
167
+ last_metadata_row = first_metadata_row + metadata_row_count - 1
168
+ for row in range(first_metadata_row, last_metadata_row):
169
+ self.grid.setRowMinimumHeight(row, GIT_EXTENSIONS_METADATA_ROW_HEIGHT)
170
+ self.grid.setRowMinimumHeight(last_metadata_row, self.last_row_height)
171
+ caption_column = 0
172
+ self.grid.setColumnMinimumWidth(caption_column, GIT_EXTENSIONS_CAPTION_COLUMN_WIDTH)
173
+ for caption_label in self.caption_labels:
174
+ caption_label.setIndent(GIT_EXTENSIONS_CAPTION_INDENT)
175
+ # The right margin is the space every captured caption keeps before its value: the column is the capture's
176
+ # width, or wider when a larger system text needs it, and then still leaves that space ("Commit date:" at
177
+ # 14 pt under macOS touched its value).
178
+ caption_label.setContentsMargins(0, 0, CAPTION_TO_FIELD_MIN_SPACE, GIT_EXTENSIONS_TEXT_LIFT)
179
+ for value_label in (self.author_label, self.date_label, self.branch_label, self.tag_label):
180
+ value_label.setContentsMargins(0, 0, 0, GIT_EXTENSIONS_TEXT_LIFT)
181
+ self.setTitle(self._short_hash(self.target_oid))
182
+ self._place_git_extensions_content()
183
+
184
+ def _short_hash(self, oid: str) -> str:
185
+ """Return the frame caption for a commit: eight digits in the Git Extensions look, seven otherwise."""
186
+ return oid[:GIT_EXTENSIONS_REVISION_PREFIX_LENGTH if self.git_extensions_look else REVISION_PREFIX_LENGTH]
187
+
188
+ def _place_git_extensions_content(self) -> None:
189
+ """Set the grid margins so the content sits at the measured distances from the painted frame.
190
+
191
+ The group box reserves its own margins for the native title and frame, and recomputes them whenever the style
192
+ or font changes; the grid margins are the measured distances minus that reservation.
193
+ """
194
+ reserved = self.contentsMargins()
195
+ line_top = self.title_rectangle().center().y()
196
+ self.grid.setContentsMargins(max(0, GIT_EXTENSIONS_CONTENT_LEFT - reserved.left()), max(0, line_top + GIT_EXTENSIONS_CONTENT_TOP_BELOW_LINE - reserved.top()),
197
+ max(0, GIT_EXTENSIONS_CONTENT_RIGHT - reserved.right()), max(0, GIT_EXTENSIONS_CONTENT_BOTTOM - reserved.bottom()))
198
+
199
+ def changeEvent(self, event: QEvent) -> None:
200
+ """Keep the Git Extensions spacing when a new style or font changes the group box's own margins."""
201
+ super().changeEvent(event)
202
+ # The group box sends style and font changes while it is still being built, before the look is known.
203
+ if not getattr(self, "git_extensions_look", False) or not hasattr(self, "caption_labels"):
204
+ return
205
+ if event.type() in (QEvent.Type.StyleChange, QEvent.Type.FontChange):
206
+ self._place_git_extensions_content()
207
+
208
+ def _label(self, text: str, *, bold: bool = False) -> QLabel:
209
+ """Build a value label: one line cut with an ellipsis, or literal Git text able to wrap without widening the form."""
210
+ if self.single_line:
211
+ label = ElidedLabel(text)
212
+ else:
213
+ label = WrappingLabel(text)
214
+ label.setWordWrap(True)
215
+ font = label.font()
216
+ font.setBold(bold)
217
+ label.setFont(font)
218
+ return label
219
+
220
+ def retarget(self, target_oid: str) -> None:
221
+ """Show another commit: its short hash in the frame and pending values until its details and references arrive.
222
+
223
+ An answer still expected for the previous commit is ignored afterwards, since it no longer matches the target.
224
+ """
225
+ if target_oid == self.target_oid:
226
+ return
227
+ self.target_oid = target_oid
228
+ self.setToolTip(target_oid)
229
+ self.setAccessibleName("Target revision " + target_oid)
230
+ self.setTitle(self._short_hash(target_oid))
231
+ self.subject_label.setText(LOADING_COMMIT_TEXT)
232
+ for label in (self.author_label, self.date_label, self.branch_label, self.tag_label):
233
+ label.setText(NOT_AVAILABLE_TEXT)
234
+ self._show_reference_presence(has_branch=False, has_tag=False)
235
+
236
+ def show_commit(self, fields: tuple[str, ...]) -> None:
237
+ """Show only the subject, author and real commit date of the captured target."""
238
+ oid, author, _author_date, _committer, commit_date, message = fields
239
+ if oid != self.target_oid:
240
+ return
241
+ timestamp = None
242
+ if commit_date:
243
+ try:
244
+ timestamp = int(datetime.fromisoformat(commit_date).timestamp())
245
+ except (ValueError, OverflowError, OSError):
246
+ timestamp = None
247
+ self._show_values(message, author, timestamp, commit_date)
248
+
249
+ def show_loaded_commit(self, commit: Commit) -> None:
250
+ """Show the subject, author and commit date of a commit the history graph already holds, without Git."""
251
+ if commit.oid != self.target_oid:
252
+ return
253
+ if commit.time_unavailable:
254
+ self._show_values(commit.message, commit.author_name, None, COMMIT_TIME_UNAVAILABLE_CAUSE)
255
+ return
256
+ commit_date = datetime.fromtimestamp(commit.committer_timestamp, UTC).isoformat()
257
+ self._show_values(commit.message, commit.author_name, commit.committer_timestamp, commit_date)
258
+
259
+ def _show_values(self, message: str, author: str, timestamp: int | None, commit_date: str) -> None:
260
+ """Write the subject line, the author and the commit date the way the history graph writes dates.
261
+
262
+ Wrapping labels also carry the whole message and the exact date as tooltips; one-line labels keep their
263
+ tooltip for the whole text of a value they had to cut.
264
+ """
265
+ subject, _, _body = message.partition("\n")
266
+ self.subject_label.setText(subject or NOT_AVAILABLE_TEXT)
267
+ self.author_label.setText(author or NOT_AVAILABLE_TEXT)
268
+ if not commit_date:
269
+ self.date_label.setText(NOT_AVAILABLE_TEXT)
270
+ elif timestamp is None:
271
+ self.date_label.setText(DATE_UNAVAILABLE_TEXT)
272
+ else:
273
+ self.date_label.setText(format_commit_date(timestamp, self.clock()))
274
+ if self.single_line:
275
+ return
276
+ self.subject_label.setToolTip(message)
277
+ self.date_label.setToolTip(commit_date)
278
+
279
+ def show_references(self, snapshot: ReferenceSnapshot) -> None:
280
+ """List real branches and tags at the revision, excluding redundant symbolic aliases."""
281
+ branches: list[str] = []
282
+ tags: list[str] = []
283
+ for reference in sorted(snapshot.references, key=lambda reference: reference.full_name):
284
+ if reference.target_oid != self.target_oid or reference.symbolic_target:
285
+ continue
286
+ name = reference.full_name.removeprefix(reference.kind.prefix)
287
+ if reference.kind == ReferenceKind.TAGS:
288
+ tags.append(name)
289
+ elif reference.kind in (ReferenceKind.HEADS, ReferenceKind.REMOTES):
290
+ branches.append(name)
291
+ self.branch_label.setText(", ".join(branches) or NOT_AVAILABLE_TEXT)
292
+ self.tag_label.setText(", ".join(tags) or NOT_AVAILABLE_TEXT)
293
+ self._show_reference_presence(has_branch=bool(branches), has_tag=bool(tags))
294
+
295
+ def _show_reference_presence(self, *, has_branch: bool, has_tag: bool) -> None:
296
+ """Remember which bands are filled and, in the Git Extensions look, write present references in bold."""
297
+ self.has_branch = has_branch
298
+ self.has_tag = has_tag
299
+ if not self.git_extensions_look:
300
+ return
301
+ # Git Extensions writes a present branch or tag in bold on its colored band.
302
+ for label, present in ((self.branch_label, self.has_branch), (self.tag_label, self.has_tag)):
303
+ font = label.font()
304
+ font.setBold(present)
305
+ label.setFont(font)
306
+ self.update()
307
+
308
+ def branch_band_color(self, present: bool) -> QColor:
309
+ """Return the Branch(es) band color: salmon when the commit carries a branch, the control grey otherwise."""
310
+ if not present:
311
+ return self._empty_band_color()
312
+ return QColor(BRANCH_BAND_DARK_COLOR if is_dark_palette(self.palette()) else BRANCH_BAND_LIGHT_COLOR)
313
+
314
+ def tag_band_color(self, present: bool) -> QColor:
315
+ """Return the Tag(s) band color: steel blue when the commit carries a tag, the control grey otherwise."""
316
+ if not present:
317
+ return self._empty_band_color()
318
+ return QColor(TAG_BAND_DARK_COLOR if is_dark_palette(self.palette()) else TAG_BAND_LIGHT_COLOR)
319
+
320
+ def _empty_band_color(self) -> QColor:
321
+ """Use the captured control grey in a light theme and the palette's control color in a dark one."""
322
+ if is_dark_palette(self.palette()):
323
+ return self.palette().color(QPalette.ColorRole.Button)
324
+ return QColor(EMPTY_REFERENCE_BAND_LIGHT_COLOR)
325
+
326
+ def paintEvent(self, event: QPaintEvent) -> None:
327
+ """Draw the Git Extensions frame and reference bands, or the native frame when that look is off."""
328
+ if not self.git_extensions_look:
329
+ QGroupBox.paintEvent(self, event)
330
+ return
331
+ super().paintEvent(event)
332
+ painter = QPainter(self)
333
+ # Each band also covers half of the row spacing above and below its value, so consecutive bands touch like
334
+ # the Git Extensions rows, and it starts a little left of the text so the value does not touch its edge.
335
+ row_spacing = max(0, self.grid.verticalSpacing())
336
+ space_above, space_below = row_spacing // 2, row_spacing - row_spacing // 2
337
+ for label, color in ((self.branch_label, self.branch_band_color(self.has_branch)), (self.tag_label, self.tag_band_color(self.has_tag))):
338
+ value = label.geometry()
339
+ painter.fillRect(QRect(QPoint(value.left() - VALUE_BAND_LEAD, value.top() - space_above), QPoint(value.right(), value.bottom() + space_below)), color)