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,196 @@
1
+ """Parse a for-each-ref listing into one frozen reference snapshot, without starting Git.
2
+
3
+ The listing itself is read by ``ReferenceService`` of ``gitpane.reference.reference_edit``, with the format and the
4
+ parser defined here.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, replace
10
+ from dataclasses import field as dataclass_field
11
+ from typing import TYPE_CHECKING
12
+
13
+ from gitpane.config import LOCAL_PREFIX, REMOTE_PREFIX
14
+ from gitpane.history.repository_value import Reference, ReferenceKind, validate_oid
15
+
16
+ if TYPE_CHECKING:
17
+ from gitpane.history.repository_value import RepositoryContext
18
+ from gitpane.reference.stash_list import StashList
19
+
20
+ # One for-each-ref line per reference: its full name, its object, the peeled object and both types, its symbolic
21
+ # target, the marker Git prints when HEAD is that very reference, then the upstream Git resolves for a local branch
22
+ # and the remote that upstream belongs to. Git leaves both upstream fields empty for anything but a local branch with
23
+ # a usable upstream, and names the remote "." when the upstream is another local branch. Every reader of references
24
+ # uses this one format and the indices below, so no second copy of the field order can drift from the parser, and the
25
+ # upstream comes with the same single listing instead of another Git process.
26
+ REFERENCE_FORMAT = ("%(refname)%00%(objectname)%00%(*objectname)%00%(objecttype)%00%(*objecttype)%00%(symref)%00%(HEAD)"
27
+ "%00%(upstream)%00%(upstream:remotename)")
28
+ REFERENCE_NAME_FIELD_INDEX = 0
29
+ REFERENCE_OBJECT_OID_FIELD_INDEX = 1
30
+ REFERENCE_PEELED_OID_FIELD_INDEX = 2
31
+ REFERENCE_OBJECT_TYPE_FIELD_INDEX = 3
32
+ REFERENCE_PEELED_TYPE_FIELD_INDEX = 4
33
+ REFERENCE_SYMBOLIC_TARGET_FIELD_INDEX = 5
34
+ REFERENCE_HEAD_MARKER_FIELD_INDEX = 6
35
+ REFERENCE_UPSTREAM_FIELD_INDEX = 7
36
+ REFERENCE_UPSTREAM_REMOTE_FIELD_INDEX = 8
37
+ REFERENCE_FIELD_COUNT = 9
38
+
39
+
40
+ # The remote name Git gives an upstream that is a branch of this very repository: no remote to push to.
41
+ LOCAL_UPSTREAM_REMOTE_NAME = "."
42
+ REFERENCE_FIELD_SEPARATOR = "\0"
43
+ REFERENCE_LINE_TERMINATOR = b"\n"
44
+ CURRENT_HEAD_MARKER = "*"
45
+ OTHER_HEAD_MARKER = " "
46
+ COMMIT_OBJECT_TYPE = "commit"
47
+ TAG_OBJECT_TYPE = "tag"
48
+
49
+
50
+ def reference_kind(full_name: str) -> ReferenceKind:
51
+ """Group a full reference name the way the window lists it: local, remote, and everything else with tags."""
52
+ if full_name.startswith(LOCAL_PREFIX):
53
+ return ReferenceKind.HEADS
54
+ if full_name.startswith(REMOTE_PREFIX):
55
+ return ReferenceKind.REMOTES
56
+ return ReferenceKind.TAGS
57
+
58
+
59
+ def short_reference_name(full_name: str) -> str:
60
+ """Write a full reference name the way the window shows it: without its refs/heads/, refs/remotes/ or refs/tags/ prefix."""
61
+ return full_name.removeprefix(reference_kind(full_name).prefix)
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class ReferenceRead:
66
+ """Everything one for-each-ref listing written with REFERENCE_FORMAT proves, for the reference lists and the history graph.
67
+
68
+ The first four fields mean exactly what parse_reference_output answers. The commit set names every reference whose
69
+ own object, or the object its tag peels to, is a commit. The unresolved tags are those Git could only peel to
70
+ another tag: each full name is kept with its own tag object, which a separate lookup still has to follow. The two
71
+ upstream maps hold, for each local branch that has one, the full name of its upstream and the remote that upstream
72
+ belongs to; a branch without an upstream is absent from both, and one following another local branch is absent
73
+ from the remote map.
74
+ """
75
+
76
+ references: tuple[Reference, ...]
77
+ object_oid_by_name: dict[str, str]
78
+ head_oid: str | None
79
+ head_ref: str | None
80
+ commit_reference_name_set: frozenset[str]
81
+ unresolved_tag_oid_by_name: dict[str, str]
82
+ upstream_by_name: dict[str, str] = dataclass_field(default_factory=dict)
83
+ upstream_remote_by_name: dict[str, str] = dataclass_field(default_factory=dict)
84
+
85
+
86
+ def parse_reference_output(output: bytes) -> tuple[tuple[Reference, ...], dict[str, str], str | None, str | None]:
87
+ """Read a for-each-ref listing written with REFERENCE_FORMAT: references, the object of each name, HEAD's commit and branch.
88
+
89
+ This is parse_reference_read reduced to the four values every reader of references started with.
90
+ """
91
+ read = parse_reference_read(output)
92
+ return read.references, read.object_oid_by_name, read.head_oid, read.head_ref
93
+
94
+
95
+ def parse_reference_read(output: bytes) -> ReferenceRead:
96
+ """Read a for-each-ref listing written with REFERENCE_FORMAT, whatever the order of its lines.
97
+
98
+ Answers the references, the object each name holds, and HEAD's commit and branch when the listing itself
99
+ proves them: only a local branch holding a commit and carrying Git's current marker names HEAD. A detached or
100
+ unborn HEAD marks nothing, and a marker on anything else is not trusted either; both answer None for HEAD, so
101
+ the caller asks Git directly instead of guessing. A cut-off, malformed, duplicated or doubly marked listing is
102
+ refused, because a partial list of references would look exactly like a complete one to the window.
103
+ """
104
+ if output and not output.endswith(REFERENCE_LINE_TERMINATOR):
105
+ raise ValueError("Incomplete reference data returned by Git")
106
+ records = output.split(REFERENCE_LINE_TERMINATOR)
107
+ records.pop()
108
+ references: list[Reference] = []
109
+ object_oid_by_name: dict[str, str] = {}
110
+ commit_reference_name_set: set[str] = set()
111
+ unresolved_tag_oid_by_name: dict[str, str] = {}
112
+ upstream_by_name: dict[str, str] = {}
113
+ upstream_remote_by_name: dict[str, str] = {}
114
+ marked_name = None
115
+ for record in records:
116
+ # The HEAD marker is a single space and the upstream fields are empty for most references: the line is split as
117
+ # it is, never trimmed, so a blank field is never mistaken for a missing one.
118
+ fields = record.decode("utf-8", "surrogateescape").split(REFERENCE_FIELD_SEPARATOR)
119
+ if len(fields) != REFERENCE_FIELD_COUNT:
120
+ raise ValueError("Incomplete reference data returned by Git")
121
+ full_name = fields[REFERENCE_NAME_FIELD_INDEX]
122
+ object_oid = fields[REFERENCE_OBJECT_OID_FIELD_INDEX]
123
+ peeled_oid = fields[REFERENCE_PEELED_OID_FIELD_INDEX]
124
+ object_type = fields[REFERENCE_OBJECT_TYPE_FIELD_INDEX]
125
+ peeled_type = fields[REFERENCE_PEELED_TYPE_FIELD_INDEX]
126
+ symbolic_target = fields[REFERENCE_SYMBOLIC_TARGET_FIELD_INDEX]
127
+ head_marker = fields[REFERENCE_HEAD_MARKER_FIELD_INDEX]
128
+ if not full_name or not object_type or head_marker not in (CURRENT_HEAD_MARKER, OTHER_HEAD_MARKER):
129
+ raise ValueError("Incomplete reference data returned by Git")
130
+ validate_oid(object_oid)
131
+ if peeled_oid:
132
+ validate_oid(peeled_oid)
133
+ if full_name in object_oid_by_name:
134
+ raise ValueError("Git listed the same reference twice")
135
+ if head_marker == CURRENT_HEAD_MARKER:
136
+ if marked_name is not None:
137
+ raise ValueError("Git marked more than one reference as HEAD")
138
+ marked_name = full_name
139
+ target_oid = object_oid if object_type == COMMIT_OBJECT_TYPE else peeled_oid if peeled_type == COMMIT_OBJECT_TYPE and peeled_oid else None
140
+ object_oid_by_name[full_name] = object_oid
141
+ references.append(Reference(full_name, reference_kind(full_name), target_oid, symbolic_target or None, False))
142
+ if target_oid is not None:
143
+ commit_reference_name_set.add(full_name)
144
+ elif object_type == TAG_OBJECT_TYPE and peeled_type == TAG_OBJECT_TYPE:
145
+ unresolved_tag_oid_by_name[full_name] = object_oid
146
+ if full_name.startswith(LOCAL_PREFIX):
147
+ upstream = fields[REFERENCE_UPSTREAM_FIELD_INDEX]
148
+ upstream_remote = fields[REFERENCE_UPSTREAM_REMOTE_FIELD_INDEX]
149
+ if upstream:
150
+ upstream_by_name[full_name] = upstream
151
+ if upstream_remote and upstream_remote != LOCAL_UPSTREAM_REMOTE_NAME:
152
+ upstream_remote_by_name[full_name] = upstream_remote
153
+ head_oid = None
154
+ head_ref = None
155
+ if marked_name is not None:
156
+ marked = next(reference for reference in references if reference.full_name == marked_name)
157
+ if marked.kind == ReferenceKind.HEADS and marked.symbolic_target is None and marked.target_oid == object_oid_by_name[marked_name]:
158
+ head_oid = marked.target_oid
159
+ head_ref = marked_name
160
+ return ReferenceRead(mark_head(references, head_ref), object_oid_by_name, head_oid, head_ref, frozenset(commit_reference_name_set), unresolved_tag_oid_by_name,
161
+ upstream_by_name, upstream_remote_by_name)
162
+
163
+
164
+ def mark_head(references: list[Reference] | tuple[Reference, ...], head_ref: str | None) -> tuple[Reference, ...]:
165
+ """Flag the one reference HEAD is on, and only that one."""
166
+ marked_references: list[Reference] = []
167
+ for reference in references:
168
+ is_head = head_ref is not None and reference.full_name == head_ref
169
+ marked_references.append(reference if reference.is_head == is_head else replace(reference, is_head=is_head))
170
+ return tuple(marked_references)
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class ReferenceSnapshot:
175
+ """Freeze refs, HEAD and worktree membership from one consistent native read."""
176
+
177
+ context: RepositoryContext
178
+ git_filename: str
179
+ generation: int
180
+ references: tuple[Reference, ...]
181
+ object_oid_by_name: dict[str, str]
182
+ head_oid: str | None
183
+ head_ref: str | None
184
+ worktrees: bytes
185
+ signature: tuple
186
+ remote_names: tuple[str, ...] = ()
187
+ # The listing this capture was built from, so the history graph can use the same observation instead of listing
188
+ # the references again. It is derived from the fields above and never compared: two captures of the same state
189
+ # are equal whether or not they carry it.
190
+ reference_read: ReferenceRead | None = dataclass_field(default=None, compare=False)
191
+ # Upstream of each local branch that has one, and the remote of that upstream, from the same listing.
192
+ upstream_by_name: dict[str, str] = dataclass_field(default_factory=dict)
193
+ upstream_remote_by_name: dict[str, str] = dataclass_field(default_factory=dict)
194
+ # The stashes, read in the same pass for the left panel of a window. Only reads made to show a window carry it,
195
+ # and it never takes part in comparing two captures: the stash reflog is not one of the files the signature covers.
196
+ stash_list: StashList | None = dataclass_field(default=None, compare=False)