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
File without changes
@@ -0,0 +1,102 @@
1
+ """Read Git's refusal to check out a path of a submodule.
2
+
3
+ Git writes two whole lines when the checkout of a submodule refuses a path: the refused path, then the submodule it
4
+ could not check out. Only those whole lines count, never the same words inside another sentence, so the output of a
5
+ server or of a hook cannot make GitPane believe a checkout was refused.
6
+
7
+ Git writes the same fatal line for any checkout of a submodule that fails, on a locked index, a name too long for the
8
+ file system or a full disk; such a fatal line counts only after the start of a line that cites a refused path. A failure
9
+ of another kind adds no folder to the refused ones and stays a failure that Retry submodules takes up again.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import re
16
+ from dataclasses import dataclass
17
+
18
+ from gitpane.config import GIT_PATH_SEPARATOR
19
+ from gitpane.system.path_identity import canonical_path
20
+
21
+ # Git's own two lines when the checkout of a submodule refuses a path: the refused path, then the submodule that could
22
+ # not be checked out. A fatal line counts only after the start of a refused path's line, since Git writes it for any
23
+ # failed checkout of a submodule. Only a whole line counts, so a server's sentence that quotes these words does not. A
24
+ # file name that holds a line break can still make these two lines; it adds at most one folder GitPane leaves unwritten,
25
+ # so nothing is ever written because of them. Git writes the line break of a refused path as it is, which cuts the first
26
+ # line in two: that path gets no sentence of GitPane's, but the start of its line stays at the start of a line, and its
27
+ # submodule is still recognised by the fatal line that follows, which stays whole. The path is matched up to the last
28
+ # quote, since a file name may itself hold a quote.
29
+ INVALID_PATH_START = "error: invalid path '"
30
+ INVALID_PATH_PATTERN = re.compile("^" + re.escape(INVALID_PATH_START) + r"(?P<path>.*)'$")
31
+ SUBMODULE_CHECKOUT_FAILED_PATTERN = re.compile(r"^fatal: Unable to checkout '(?P<oid>[0-9a-f]+)' in submodule path '(?P<submodule>.*)'$")
32
+
33
+
34
+ def _git_lines(stderr_text: str) -> list[str]:
35
+ """Split Git's error output into lines, Windows line ends and the carriage returns of progress lines becoming plain line ends."""
36
+ return stderr_text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class CheckoutRefusal:
41
+ """Name a submodule Git could not check out and the paths it refused there, in the order Git printed them."""
42
+
43
+ submodule_path: str
44
+ refused_paths: tuple[str, ...]
45
+
46
+
47
+ def checkout_refusals(stderr_text: str) -> tuple[CheckoutRefusal, ...]:
48
+ """Pair every refused path Git printed with the submodule checkout failure that follows it.
49
+
50
+ Windows line ends and the carriage returns that end Git's progress lines become plain line ends first, so each line
51
+ is matched whole on every system. A checkout failure with no refused path before it says nothing about a file name
52
+ and gives no refusal.
53
+ """
54
+ refusals: list[CheckoutRefusal] = []
55
+ pending_paths: list[str] = []
56
+ for line in _git_lines(stderr_text):
57
+ invalid = INVALID_PATH_PATTERN.match(line)
58
+ if invalid is not None:
59
+ pending_paths.append(invalid.group("path"))
60
+ continue
61
+ failed = SUBMODULE_CHECKOUT_FAILED_PATTERN.match(line)
62
+ if failed is not None and pending_paths:
63
+ refusals.append(CheckoutRefusal(failed.group("submodule"), tuple(pending_paths)))
64
+ pending_paths = []
65
+ return tuple(refusals)
66
+
67
+
68
+ def refused_checkout_submodule_paths(stderr_text: str) -> tuple[str, ...]:
69
+ """Return the path of every submodule whose checkout Git refused on a file name, in the order Git printed them.
70
+
71
+ A submodule counts when a line that starts as Git's refused path line comes before its whole fatal line, even when a
72
+ line break in the refused path cut that line in two: its submodule must then be left as Git left it. The fatal line
73
+ alone is not enough, since Git writes it for every checkout of a submodule that fails, on a locked index, a name too
74
+ long for the file system or a full disk; such a submodule is not refused and stays to be checked out again. Each
75
+ fatal line ends what was read for its submodule, so a refusal never carries over to the next one.
76
+ """
77
+ submodule_paths: list[str] = []
78
+ refusal_started = False
79
+ for line in _git_lines(stderr_text):
80
+ if line.startswith(INVALID_PATH_START):
81
+ refusal_started = True
82
+ continue
83
+ failed = SUBMODULE_CHECKOUT_FAILED_PATTERN.match(line)
84
+ if failed is None:
85
+ continue
86
+ if refusal_started:
87
+ submodule_paths.append(failed.group("submodule"))
88
+ refusal_started = False
89
+ return tuple(submodule_paths)
90
+
91
+
92
+ def refused_submodule_dirs(submodule_paths: tuple[str, ...], base_dir: str) -> list[str]:
93
+ """Return the folders of the submodules whose checkout Git refused, so a later checkout of the others can leave them alone.
94
+
95
+ ``submodule_paths`` are the submodules as Git names them in its refusal, by their path from the repository it ran in,
96
+ nested submodules included, so ``base_dir`` is the folder of that repository. The folders are canonical, to compare
97
+ with the folders a submodule read gives.
98
+ """
99
+ directories = []
100
+ for submodule_path in submodule_paths:
101
+ directories.append(canonical_path(os.path.join(base_dir, *submodule_path.split(GIT_PATH_SEPARATOR))))
102
+ return directories
@@ -0,0 +1,111 @@
1
+ """Say in plain words why Git refused to check out a path of a submodule.
2
+
3
+ A submodule whose checkout Git refused is explained from the lines Git wrote, naming only the paths Git cited, with no
4
+ file name rule of GitPane's own: the rules of Windows are read by Python's ntpath, and only to pick which reason a path
5
+ Git already refused gets.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ntpath
11
+ import unicodedata
12
+ from typing import TYPE_CHECKING
13
+
14
+ from gitpane.config import CONTROL_CHARACTER_CATEGORY, GIT_PATH_SEPARATOR, WINDOWS_PLATFORM, SharedText
15
+ from gitpane.process.git_output_text import GIT_ERROR_DECODE_ERRORS
16
+
17
+ if TYPE_CHECKING:
18
+ from gitpane.submodule.checkout_refusal import CheckoutRefusal
19
+
20
+ # A control character left in a path Git cited is written as \x and two hexadecimal digits, so it can neither move the
21
+ # console's cursor nor vanish from the text. Every control character of Unicode fits in two digits.
22
+ CONTROL_CHARACTER_ESCAPE_FORMAT = r"\x{code:02x}"
23
+ # A byte that was not UTF-8 in Git's error output is kept as one character from U+DC80 to U+DCFF, which no console or
24
+ # log could write: it is written back as the byte it stands for, in the same \x form.
25
+ UNDECODED_BYTE_FIRST_CODE = 0xDC80
26
+ UNDECODED_BYTE_LAST_CODE = 0xDCFF
27
+ UNDECODED_BYTE_INDEX = 0
28
+ # The folder names Git never writes inside a repository, on every system, compared without case as Git does. git~1 is
29
+ # the short name Windows can give a .git folder.
30
+ PROTECTED_FOLDER_NAME_SET = frozenset({".git", "git~1"})
31
+ # The file names Git refuses to write as a symbolic link, on every system. Git refuses them only as a link, so a refusal
32
+ # of one of these names means a link.
33
+ PROTECTED_LINK_NAME_SET = frozenset({".gitmodules", ".gitattributes", ".gitignore", ".mailmap"})
34
+ # The characters Windows drops from the end of a name, so ".git." and ".git " name the .git folder there; Git compares
35
+ # without them on every system.
36
+ WINDOWS_DROPPED_NAME_END = ". "
37
+ LAST_COMPONENT_INDEX = -1
38
+ # What Git writes in its error lines for each control character of a path, except the tab and the line break.
39
+ GIT_CONTROL_CHARACTER_MARK = "?"
40
+ CHECKOUT_REFUSAL_PART_SEPARATOR = " "
41
+ CHECKOUT_REFUSAL_LINE_SEPARATOR = "\n"
42
+
43
+
44
+ def displayed_git_path(path_text: str) -> str:
45
+ r"""Return a path Git cited as plain text that shows every character, for the console and the forms.
46
+
47
+ Each control character, and each byte that was not UTF-8 and stayed undecoded, is written as \x and two hexadecimal
48
+ digits. Everything else is kept, including a \xNN text the path really holds and the replacement character a
49
+ repository name may really hold.
50
+ """
51
+ characters: list[str] = []
52
+ for character in path_text:
53
+ if UNDECODED_BYTE_FIRST_CODE <= ord(character) <= UNDECODED_BYTE_LAST_CODE:
54
+ byte = character.encode("utf-8", GIT_ERROR_DECODE_ERRORS)[UNDECODED_BYTE_INDEX]
55
+ characters.append(CONTROL_CHARACTER_ESCAPE_FORMAT.format(code=byte))
56
+ continue
57
+ if unicodedata.category(character) == CONTROL_CHARACTER_CATEGORY:
58
+ characters.append(CONTROL_CHARACTER_ESCAPE_FORMAT.format(code=ord(character)))
59
+ continue
60
+ characters.append(character)
61
+ return "".join(characters)
62
+
63
+
64
+ def _protected_name(component: str) -> str:
65
+ """Return a folder name as Git compares it with the folder names it protects: without case, without the dots and spaces Windows drops at its end."""
66
+ return component.rstrip(WINDOWS_DROPPED_NAME_END).lower()
67
+
68
+
69
+ def checkout_refusal_reason(path_text: str, platform: str) -> SharedText:
70
+ """Say why Git refused to write a path on the given system, named by the caller as Python names the running one.
71
+
72
+ A .git component, or a name Windows reads as one, is refused by Git itself on every system, so it is never put on
73
+ the file system. Under Windows, a path with a part Windows refuses is then put on Windows, even when it ends with a
74
+ name Git protects. A path whose last part is exactly .gitmodules, .gitattributes, .gitignore or .mailmap, without
75
+ case, is refused by Git too, as a symbolic link. Otherwise Windows is named only on Windows, where its file name
76
+ rules are the usual cause; elsewhere the reason stays neutral.
77
+ """
78
+ components = path_text.split(GIT_PATH_SEPARATOR)
79
+ for component in components:
80
+ if _protected_name(component) in PROTECTED_FOLDER_NAME_SET:
81
+ return SharedText.CHECKOUT_REFUSED_PROTECTED_PATH
82
+ # Python's own reading of the Windows name rules: dots and spaces at the end of a part, forbidden characters such as
83
+ # : and ?, device names such as CON. ntpath reads a start like "x:" as a drive and a backslash as a separator, so those
84
+ # two forms keep the reason of the link; no repository has a reason to hold them.
85
+ if platform == WINDOWS_PLATFORM and ntpath.isreserved(path_text):
86
+ return SharedText.CHECKOUT_REFUSED_WINDOWS_NAME
87
+ if components[LAST_COMPONENT_INDEX].lower() in PROTECTED_LINK_NAME_SET:
88
+ return SharedText.CHECKOUT_REFUSED_PROTECTED_LINK
89
+ if platform == WINDOWS_PLATFORM:
90
+ return SharedText.CHECKOUT_REFUSED_WINDOWS_NAME
91
+ return SharedText.CHECKOUT_REFUSED_OTHER_NAME
92
+
93
+
94
+ def checkout_refusal_text(refusals: tuple[CheckoutRefusal, ...], platform: str) -> str:
95
+ """Return the sentence that explains each submodule Git could not check out, one line per submodule, or an empty text.
96
+
97
+ Each line names the submodule, gives one reason per path Git cited, in Git's order, and ends by saying that other
98
+ files may be in the same case and that the rest of the clone is kept. Only what Git wrote is named. Git replaces
99
+ each control character of its error lines by ?, except the tab and the line break, on every system: a cited path
100
+ that holds ? gets a sentence that says so after its reason, and no other path does.
101
+ """
102
+ lines: list[str] = []
103
+ for refusal in refusals:
104
+ reasons: list[str] = []
105
+ for path in refusal.refused_paths:
106
+ reasons.append(checkout_refusal_reason(path, platform).value.format(path=displayed_git_path(path)))
107
+ if GIT_CONTROL_CHARACTER_MARK in path:
108
+ reasons.append(SharedText.CHECKOUT_REFUSED_QUESTION_MARK.value)
109
+ start = SharedText.SUBMODULE_CHECKOUT_REFUSED_START.value.format(submodule=displayed_git_path(refusal.submodule_path))
110
+ lines.append(start + CHECKOUT_REFUSAL_PART_SEPARATOR.join(reasons) + CHECKOUT_REFUSAL_PART_SEPARATOR + SharedText.SUBMODULE_CHECKOUT_REFUSED_END.value)
111
+ return CHECKOUT_REFUSAL_LINE_SEPARATOR.join(lines)
@@ -0,0 +1,65 @@
1
+ """Tell from the disk alone whether Git cloned a submodule but never checked out its files.
2
+
3
+ Git clones every submodule of a recursive clone first, then checks them out one by one, and stops at the first
4
+ checkout it cannot finish: the submodules after that one keep their Git folder, with the history and a HEAD, but no
5
+ index, and their folder holds nothing but the ``.git`` file that points to that Git folder. Only that exact state is
6
+ recognised here, so a write GitPane starts on such a submodule can never reach a file the user put there. No Git
7
+ process is started; any doubt answers no.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import stat
14
+
15
+ from gitpane.sequencer.replay_state import HEAD_BASENAME, LIVE_INDEX_BASENAME
16
+ from gitpane.submodule.submodule_loss import GIT_ENTRY_BASENAME
17
+
18
+ # What Git writes at the start of the .git file of a submodule whose Git folder lives in its parent's Git folder.
19
+ GIT_FILE_PREFIX = "gitdir:"
20
+ # A .git file holds one short line; anything larger is not one Git wrote, and is never read whole.
21
+ GIT_FILE_SIZE_LIMIT_BYTES = 4096
22
+
23
+
24
+ def linked_git_dir(git_filename: str) -> str:
25
+ """Return the Git folder a submodule's ``.git`` file names, made absolute, or an empty text when it is not such a file."""
26
+ metadata = os.lstat(git_filename)
27
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > GIT_FILE_SIZE_LIMIT_BYTES:
28
+ return ""
29
+ with open(git_filename, "rb") as stream:
30
+ line = stream.read(GIT_FILE_SIZE_LIMIT_BYTES).decode("utf-8").strip()
31
+ if not line.startswith(GIT_FILE_PREFIX):
32
+ return ""
33
+ git_dir = line.removeprefix(GIT_FILE_PREFIX).strip()
34
+ if not git_dir:
35
+ return ""
36
+ if not os.path.isabs(git_dir):
37
+ git_dir = os.path.join(os.path.dirname(git_filename), git_dir)
38
+ return os.path.normpath(git_dir)
39
+
40
+
41
+ def is_cloned_without_checkout(submodule_dir: str) -> bool:
42
+ """Tell whether Git cloned this submodule but never wrote its files, so checking it out cannot overwrite anything.
43
+
44
+ True only when the folder is a real folder (not a link or a junction) whose only entry is a ``.git`` file, and the
45
+ Git folder that file names has a HEAD and no index. A folder that holds any other file, a ``.git`` folder, a Git
46
+ folder with an index, or anything that cannot be read answers False.
47
+ """
48
+ try:
49
+ metadata = os.lstat(submodule_dir)
50
+ if not stat.S_ISDIR(metadata.st_mode) or getattr(metadata, "st_reparse_tag", 0):
51
+ return False
52
+ basenames = []
53
+ with os.scandir(submodule_dir) as entries:
54
+ for entry in entries:
55
+ basenames.append(entry.name)
56
+ if entry.name != GIT_ENTRY_BASENAME:
57
+ return False
58
+ if basenames != [GIT_ENTRY_BASENAME]:
59
+ return False
60
+ git_dir = linked_git_dir(os.path.join(submodule_dir, GIT_ENTRY_BASENAME))
61
+ if not git_dir:
62
+ return False
63
+ return os.path.isfile(os.path.join(git_dir, HEAD_BASENAME)) and not os.path.lexists(os.path.join(git_dir, LIVE_INDEX_BASENAME))
64
+ except (OSError, UnicodeDecodeError, ValueError):
65
+ return False