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
gitpane/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,297 @@
1
+ """Work out the account choice rows of Settings, one per service address.
2
+
3
+ Nothing here draws a widget; the Settings page and the clone form only display these rows.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from typing import TYPE_CHECKING
10
+
11
+ from gitpane.account.authentication import GENERIC_PROVIDER, check_repository_url, endpoint_origin, origin_address, provider_for_browser_origin
12
+ from gitpane.transport.clone import FILE_URL_ENDPOINT_KIND, HTTPS_ENDPOINT_KIND, LOCAL_ENDPOINT_KIND, SSH_ENDPOINT_KIND, endpoint_kind
13
+
14
+ if TYPE_CHECKING:
15
+ from gitpane.account.authentication import AccountCatalogue, AccountSelection
16
+ from gitpane.transport.remote_config import RemoteEntry
17
+
18
+ CHOOSE_DEFAULT_TEXT = "Choose a default account"
19
+ USE_GLOBAL_DEFAULT_TEXT = "Use global default — {label}"
20
+ NONE_CHOSEN_LABEL = "none chosen yet"
21
+ # First entry of a repository's account menu on the Accounts page when no account is connected for that address.
22
+ NO_ACCOUNT_CHOICE_TEXT = "No connected account for {address} — use Add account… above"
23
+ SSH_REMOTE_TEXT = "{remote} uses SSH — accounts do not apply"
24
+ # Texts of the clone form's greyed Account row, which never hides so the form keeps its size. Add account… is on
25
+ # the right of the menu there, so the address without an account is named without pointing above.
26
+ ENTER_URL_FIRST_TEXT = "Enter a repository URL first"
27
+ UNSUPPORTED_URL_TEXT = "Not an HTTPS, SSH or local repository address"
28
+ SSH_URL_TEXT = "SSH URL — accounts do not apply"
29
+ LOCAL_FOLDER_TEXT = "Local folder — accounts do not apply"
30
+ CLONE_NO_ACCOUNT_TEXT = "No connected account for {address}"
31
+ ACCOUNT_TEXT_SEPARATOR = " — "
32
+ SERVICE_TITLE_SEPARATOR = " — "
33
+ GENERIC_ACCOUNT_TEXT = "password or token"
34
+ REMOTE_NAME_SEPARATOR = ", "
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class AccountChoice:
39
+ """One account offered in a menu: the store namespace saved when it is chosen, and the text shown for it."""
40
+
41
+ namespace: str
42
+ text: str
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class GlobalRow:
47
+ """One service address of the Global block, its accounts and its saved default when that default is still connected.
48
+
49
+ ``title`` is what the row is labelled with: the address, led by the service name the user gave it.
50
+ """
51
+
52
+ origin: str
53
+ address: str
54
+ title: str
55
+ choices: tuple[AccountChoice, ...]
56
+ default_namespace: str | None
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class RepositoryRow:
61
+ """One service address used by the repository's remotes, with its menu and the repository's own saved choice.
62
+
63
+ ``default_text`` is the first menu entry: the account the Global default gives, the fact that none is chosen yet, or,
64
+ for an address without any connected account, the only entry of a disabled menu. ``chosen_namespace`` is the
65
+ repository's explicit account, None when the row follows the default; ``missing_choice`` flags an explicit choice
66
+ whose account is no longer listed.
67
+ """
68
+
69
+ origin: str
70
+ address: str
71
+ title: str
72
+ choices: tuple[AccountChoice, ...]
73
+ default_text: str
74
+ chosen_namespace: str | None
75
+ missing_choice: bool
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class CloneAccountRow:
80
+ """What the clone form's Account row shows for the URL it will contact.
81
+
82
+ ``origin`` is the HTTPS service address, empty when no account applies (no URL yet, SSH, local folder). ``row`` is
83
+ the address's menu, None without an address. ``disabled_text`` is the only entry of a greyed menu, empty when the
84
+ menu offers accounts; ``add_account_enabled`` tells whether Add account… can add one for this address.
85
+ """
86
+
87
+ origin: str
88
+ row: RepositoryRow | None
89
+ disabled_text: str
90
+ add_account_enabled: bool
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class RepositoryAccountView:
95
+ """What This repository shows for one read of the remotes: the HTTPS address rows and one note per SSH remote."""
96
+
97
+ rows: tuple[RepositoryRow, ...]
98
+ ssh_notes: tuple[str, ...]
99
+
100
+
101
+ def account_choices(catalogue: AccountCatalogue, origin: str) -> tuple[AccountChoice, ...]:
102
+ """List the connected accounts of exactly this service address, whatever their service, sorted by label then account.
103
+
104
+ A label shared by two accounts of the address gets the account name added, so the two menu entries stay distinct; a
105
+ username + password account gets its kind added instead, since its account name is that same username.
106
+ """
107
+ accounts = []
108
+ for selection in catalogue.accounts:
109
+ if selection.origin == origin:
110
+ accounts.append(selection)
111
+ accounts.sort(key=lambda selection: (selection.label.casefold(), selection.account, selection.provider))
112
+ label_count_by_label: dict[str, int] = {}
113
+ for selection in accounts:
114
+ label_count_by_label[selection.label] = label_count_by_label.get(selection.label, 0) + 1
115
+ choices = []
116
+ for selection in accounts:
117
+ text = selection.label
118
+ if label_count_by_label[selection.label] > 1:
119
+ text = selection.label + ACCOUNT_TEXT_SEPARATOR + (GENERIC_ACCOUNT_TEXT if selection.provider == GENERIC_PROVIDER else selection.account)
120
+ choices.append(AccountChoice(selection.namespace, text))
121
+ return tuple(choices)
122
+
123
+
124
+ def service_text(catalogue: AccountCatalogue, selection: AccountSelection) -> str:
125
+ """Name the service of an account as the Service column shows it.
126
+
127
+ The name the user gave the address wins; otherwise a service account shows its provider, and a username + password
128
+ account shows the provider of a well-known address (github.com, gitlab.com, bitbucket.org) or else the address itself.
129
+ """
130
+ service_name = catalogue.service_name_by_origin.get(selection.origin)
131
+ if service_name:
132
+ return service_name
133
+ if selection.provider != GENERIC_PROVIDER:
134
+ return selection.provider
135
+ provider = provider_for_browser_origin(selection.origin)
136
+ if provider is not None:
137
+ return provider
138
+ return origin_address(selection.origin)
139
+
140
+
141
+ def address_title(catalogue: AccountCatalogue, origin: str) -> str:
142
+ """Title a service address with the name the user gave it, so a self-hosted server is recognised by name; without one, the address alone."""
143
+ address = origin_address(origin)
144
+ service_name = catalogue.service_name_by_origin.get(origin)
145
+ if not service_name:
146
+ return address
147
+ return service_name + SERVICE_TITLE_SEPARATOR + address
148
+
149
+
150
+ def address_sort_key(origin: str) -> tuple[str, str]:
151
+ """Order service addresses as the user reads them, ignoring case, with the exact origin to break ties."""
152
+ return origin_address(origin).casefold(), origin
153
+
154
+
155
+ def global_rows(catalogue: AccountCatalogue) -> tuple[GlobalRow, ...]:
156
+ """Give one Global row per service address that has at least one connected account, in address order.
157
+
158
+ A saved default that names an account no longer listed is shown as no default at all: the row asks the user to
159
+ choose one instead of failing.
160
+ """
161
+ origin_set = set()
162
+ for selection in catalogue.accounts:
163
+ origin_set.add(selection.origin)
164
+ rows = []
165
+ for origin in sorted(origin_set, key=address_sort_key):
166
+ choices = account_choices(catalogue, origin)
167
+ default_namespace = catalogue.default_namespace_by_origin.get(origin)
168
+ if not offers_account(choices, default_namespace):
169
+ default_namespace = None
170
+ rows.append(GlobalRow(origin, origin_address(origin), address_title(catalogue, origin), choices, default_namespace))
171
+ return tuple(rows)
172
+
173
+
174
+ def offers_account(choices: tuple[AccountChoice, ...], namespace: str | None) -> bool:
175
+ """Tell whether this namespace is one of the offered accounts, so a stale default or draft is never shown as chosen."""
176
+ for choice in choices:
177
+ if choice.namespace == namespace:
178
+ return True
179
+ return False
180
+
181
+
182
+ def remote_sort_key(name: str) -> tuple[str, str]:
183
+ """Order remote names as the user reads them, ignoring case, with the exact name to break ties."""
184
+ return name.casefold(), name
185
+
186
+
187
+ def repository_row(catalogue: AccountCatalogue, origin: str, remote_names: tuple[str, ...]) -> RepositoryRow:
188
+ """Describe one address of the repository: its title with the remotes using it, its menu and the repository's saved choice.
189
+
190
+ Nothing raises for an explicit choice or a default naming an account no longer listed: the row simply shows it.
191
+ """
192
+ address = origin_address(origin)
193
+ title = address_title(catalogue, origin)
194
+ if remote_names:
195
+ title += " (" + REMOTE_NAME_SEPARATOR.join(remote_names) + ")"
196
+ choices = account_choices(catalogue, origin)
197
+ default_namespace = catalogue.default_namespace_by_origin.get(origin)
198
+ default_label = NONE_CHOSEN_LABEL
199
+ for choice in choices:
200
+ if choice.namespace == default_namespace:
201
+ default_label = choice.text
202
+ default_text = USE_GLOBAL_DEFAULT_TEXT.format(label=default_label) if choices else NO_ACCOUNT_CHOICE_TEXT.format(address=address)
203
+ explicit_namespace = catalogue.repository_namespace_by_origin.get(origin)
204
+ missing_choice = explicit_namespace is not None and not offers_account(choices, explicit_namespace)
205
+ chosen_namespace = None if missing_choice else explicit_namespace
206
+ return RepositoryRow(origin, address, title, choices, default_text, chosen_namespace, missing_choice)
207
+
208
+
209
+ def refused_url_account_row(endpoint: str) -> CloneAccountRow:
210
+ """Grey the clone form's Account row for a URL no clone accepts, telling an empty field apart from refused text."""
211
+ if not endpoint:
212
+ return CloneAccountRow("", None, ENTER_URL_FIRST_TEXT, False)
213
+ return CloneAccountRow("", None, UNSUPPORTED_URL_TEXT, False)
214
+
215
+
216
+ def clone_account_row(catalogue: AccountCatalogue, url: str) -> CloneAccountRow:
217
+ """Describe the clone form's Account row for a typed URL, a local folder or nothing yet.
218
+
219
+ Only an HTTPS address has accounts; the new repository then starts from the address's global default, and the menu
220
+ is greyed with the address named when no account is connected for it. A file:// URL names a local repository just
221
+ like a folder path, so it says that accounts do not apply. An empty field asks for a URL, while typed text that no
222
+ clone accepts (plain HTTP, git://, a password or query in the URL) says so instead of asking for one again.
223
+ """
224
+ endpoint = url.strip()
225
+ try:
226
+ kind = endpoint_kind(endpoint, allow_local=True)
227
+ except ValueError:
228
+ return refused_url_account_row(endpoint)
229
+ if kind == SSH_ENDPOINT_KIND:
230
+ return CloneAccountRow("", None, SSH_URL_TEXT, False)
231
+ if kind in (LOCAL_ENDPOINT_KIND, FILE_URL_ENDPOINT_KIND):
232
+ return CloneAccountRow("", None, LOCAL_FOLDER_TEXT, False)
233
+ try:
234
+ origin = endpoint_origin(endpoint)
235
+ except ValueError:
236
+ return refused_url_account_row(endpoint)
237
+ row = repository_row(catalogue, origin, ())
238
+ if not row.choices:
239
+ return CloneAccountRow(origin, row, CLONE_NO_ACCOUNT_TEXT.format(address=row.address), True)
240
+ return CloneAccountRow(origin, row, "", True)
241
+
242
+
243
+ def repository_account_view(catalogue: AccountCatalogue, remote_entries: tuple[RemoteEntry, ...]) -> RepositoryAccountView:
244
+ """Group the effective fetch and push URLs of the remotes by HTTPS service address and note every SSH remote.
245
+
246
+ A local path, a plain HTTP URL or anything Git transport would refuse gives no row: no account applies to it. Remotes
247
+ Git itself ignores are left out too, since they are never contacted.
248
+ """
249
+ remote_name_set_by_origin: dict[str, set[str]] = {}
250
+ ssh_notes = []
251
+ sorted_entries = sorted(remote_entries, key=lambda entry: remote_sort_key(entry.name))
252
+ for entry in sorted_entries:
253
+ if not entry.active:
254
+ continue
255
+ uses_ssh = False
256
+ for url in entry.fetch_urls + entry.push_urls:
257
+ try:
258
+ kind = endpoint_kind(url)
259
+ if kind == HTTPS_ENDPOINT_KIND:
260
+ remote_name_set_by_origin.setdefault(endpoint_origin(url), set()).add(entry.name)
261
+ except ValueError:
262
+ continue
263
+ if kind == SSH_ENDPOINT_KIND:
264
+ uses_ssh = True
265
+ if uses_ssh:
266
+ ssh_notes.append(SSH_REMOTE_TEXT.format(remote=entry.name))
267
+ rows = []
268
+ for origin in sorted(remote_name_set_by_origin, key=address_sort_key):
269
+ remote_names = tuple(sorted(remote_name_set_by_origin[origin], key=remote_sort_key))
270
+ rows.append(repository_row(catalogue, origin, remote_names))
271
+ return RepositoryAccountView(tuple(rows), tuple(ssh_notes))
272
+
273
+
274
+ def check_url_by_origin(remote_entries: tuple[RemoteEntry, ...]) -> dict[str, str]:
275
+ """Pick, for each HTTPS service address of the remotes, one repository URL on which a new account can be tested without asking for it.
276
+
277
+ The pick does not depend on the order Git lists the remotes in: remotes are taken by name, and within one remote
278
+ its fetch URLs come before its push URLs. The first usable URL of an address wins. Its credentials part is dropped,
279
+ so a username or token written in a remote URL is never shown or sent. Remotes Git ignores, SSH remotes and URLs
280
+ that could not be used for the test (no repository path, a password, a query) are skipped.
281
+ """
282
+ url_by_origin: dict[str, str] = {}
283
+ sorted_entries = sorted(remote_entries, key=lambda entry: remote_sort_key(entry.name))
284
+ for entry in sorted_entries:
285
+ if not entry.active:
286
+ continue
287
+ for url in entry.fetch_urls + entry.push_urls:
288
+ try:
289
+ if endpoint_kind(url) != HTTPS_ENDPOINT_KIND:
290
+ continue
291
+ origin = endpoint_origin(url)
292
+ repository_url = check_repository_url(url, origin)
293
+ except ValueError:
294
+ continue
295
+ if origin not in url_by_origin:
296
+ url_by_origin[origin] = repository_url
297
+ return url_by_origin
@@ -0,0 +1,67 @@
1
+ """Find the one connected account whose name is the owner written in a repository address.
2
+
3
+ Hosting services put the owner of a repository first in its path (``https://github.com/<owner>/<repository>.git``). When
4
+ the branches of such an address cannot be read with the account in use, the account named like that owner is the one
5
+ most likely to see them. Nothing here reads a store or draws a widget: the clone form hands the listed accounts and the
6
+ typed address, and decides itself whether to switch.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING
12
+ from urllib.parse import unquote, urlsplit
13
+
14
+ from gitpane.account.authentication import endpoint_origin
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Sequence
18
+
19
+ from gitpane.account.authentication import AccountSelection
20
+
21
+ # An address needs an owner segment and a repository segment after it; a single segment names no owner.
22
+ OWNER_ADDRESS_MINIMUM_SEGMENT_COUNT = 2
23
+ OWNER_SEGMENT_INDEX = 0
24
+ PATH_SEPARATOR = "/"
25
+
26
+
27
+ def address_owner(url: str) -> str:
28
+ """Return the owner written first in the path of an HTTPS repository address, decoded from its ``%xx`` escapes.
29
+
30
+ Only an address an account could be used for has an owner: an SSH address, a local folder, a ``file://`` URL or any
31
+ address the accounts refuse give an empty text, and so does an address with fewer than two path segments, since a
32
+ lone segment is the repository itself, not its owner.
33
+ """
34
+ try:
35
+ endpoint_origin(url)
36
+ path = urlsplit(url).path
37
+ except ValueError:
38
+ return ""
39
+ segments = []
40
+ for segment in path.split(PATH_SEPARATOR):
41
+ if segment:
42
+ segments.append(segment)
43
+ if len(segments) < OWNER_ADDRESS_MINIMUM_SEGMENT_COUNT:
44
+ return ""
45
+ return unquote(segments[OWNER_SEGMENT_INDEX])
46
+
47
+
48
+ def owner_account_namespace(accounts: Sequence[AccountSelection], url: str) -> str | None:
49
+ """Return the namespace of the only listed account of this address's service whose name is the address's owner, else None.
50
+
51
+ The name compared is the one the account menu shows (``label``), without regard to case, since hosting services
52
+ compare their user names that way. Only accounts of the same service address (scheme, host and port) are
53
+ candidates, so a token is never offered to another server than its own. Two matching accounts leave no safe choice,
54
+ so None is returned then, as when none matches or the address names no owner.
55
+ """
56
+ owner = address_owner(url)
57
+ if not owner:
58
+ return None
59
+ origin = endpoint_origin(url)
60
+ folded_owner = owner.casefold()
61
+ matching_namespace_set = set()
62
+ for selection in accounts:
63
+ if selection.origin == origin and selection.label.casefold() == folded_owner:
64
+ matching_namespace_set.add(selection.namespace)
65
+ if len(matching_namespace_set) != 1:
66
+ return None
67
+ return next(iter(matching_namespace_set))