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