git 4.3.2 → 5.0.0.beta.2

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 (297) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +73 -2705
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/pull_request_template.md +3 -1
  5. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  6. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  7. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  8. data/.github/skills/command-implementation/SKILL.md +229 -0
  9. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  10. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  11. data/.github/skills/dependency-management/SKILL.md +72 -0
  12. data/.github/skills/development-workflow/SKILL.md +506 -0
  13. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  14. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  15. data/.github/skills/extract-facade-from-base-lib/SKILL.md +600 -0
  16. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  17. data/.github/skills/facade-implementation/SKILL.md +274 -0
  18. data/.github/skills/facade-test-conventions/SKILL.md +394 -0
  19. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  20. data/.github/skills/make-skill-template/SKILL.md +176 -0
  21. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  22. data/.github/skills/project-context/SKILL.md +313 -0
  23. data/.github/skills/pull-request-review/SKILL.md +168 -0
  24. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  25. data/.github/skills/release-management/SKILL.md +125 -0
  26. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  27. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  28. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  29. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  30. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  31. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  32. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  33. data/.github/skills/test-debugging/SKILL.md +160 -0
  34. data/.github/skills/yard-documentation/SKILL.md +793 -0
  35. data/.github/workflows/continuous_integration.yml +3 -2
  36. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  37. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  38. data/.github/workflows/release.yml +3 -4
  39. data/.gitignore +8 -0
  40. data/.husky/pre-commit +13 -0
  41. data/.release-please-manifest.json +1 -1
  42. data/.rspec +3 -0
  43. data/.rubocop.yml +12 -3
  44. data/.rubocop_todo.yml +23 -5
  45. data/.yardopts +1 -0
  46. data/CHANGELOG.md +0 -40
  47. data/CONTRIBUTING.md +694 -53
  48. data/README.md +67 -15
  49. data/Rakefile +61 -9
  50. data/UPGRADING.md +141 -0
  51. data/commitlint.test +4 -0
  52. data/git.gemspec +18 -7
  53. data/lib/git/args_builder.rb +0 -8
  54. data/lib/git/branch.rb +369 -43
  55. data/lib/git/branch_delete_failure.rb +31 -0
  56. data/lib/git/branch_delete_result.rb +63 -0
  57. data/lib/git/branch_info.rb +178 -0
  58. data/lib/git/branches.rb +122 -24
  59. data/lib/git/command_line/base.rb +255 -0
  60. data/lib/git/command_line/capturing.rb +251 -0
  61. data/lib/git/command_line/result.rb +96 -0
  62. data/lib/git/command_line/streaming.rb +196 -0
  63. data/lib/git/command_line.rb +43 -322
  64. data/lib/git/command_line_result.rb +4 -88
  65. data/lib/git/commands/add.rb +131 -0
  66. data/lib/git/commands/am/abort.rb +43 -0
  67. data/lib/git/commands/am/apply.rb +252 -0
  68. data/lib/git/commands/am/continue.rb +43 -0
  69. data/lib/git/commands/am/quit.rb +43 -0
  70. data/lib/git/commands/am/retry.rb +47 -0
  71. data/lib/git/commands/am/show_current_patch.rb +64 -0
  72. data/lib/git/commands/am/skip.rb +42 -0
  73. data/lib/git/commands/am.rb +33 -0
  74. data/lib/git/commands/apply.rb +237 -0
  75. data/lib/git/commands/archive/list_formats.rb +46 -0
  76. data/lib/git/commands/archive.rb +140 -0
  77. data/lib/git/commands/arguments.rb +3510 -0
  78. data/lib/git/commands/base.rb +404 -0
  79. data/lib/git/commands/branch/copy.rb +94 -0
  80. data/lib/git/commands/branch/create.rb +173 -0
  81. data/lib/git/commands/branch/delete.rb +80 -0
  82. data/lib/git/commands/branch/list.rb +162 -0
  83. data/lib/git/commands/branch/move.rb +94 -0
  84. data/lib/git/commands/branch/set_upstream.rb +86 -0
  85. data/lib/git/commands/branch/show_current.rb +49 -0
  86. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  87. data/lib/git/commands/branch.rb +34 -0
  88. data/lib/git/commands/cat_file/batch.rb +369 -0
  89. data/lib/git/commands/cat_file/filtered.rb +105 -0
  90. data/lib/git/commands/cat_file/raw.rb +216 -0
  91. data/lib/git/commands/cat_file.rb +49 -0
  92. data/lib/git/commands/checkout/branch.rb +151 -0
  93. data/lib/git/commands/checkout/files.rb +115 -0
  94. data/lib/git/commands/checkout.rb +38 -0
  95. data/lib/git/commands/checkout_index.rb +105 -0
  96. data/lib/git/commands/clean.rb +100 -0
  97. data/lib/git/commands/clone.rb +240 -0
  98. data/lib/git/commands/commit.rb +272 -0
  99. data/lib/git/commands/commit_tree.rb +100 -0
  100. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  101. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  102. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  103. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  104. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  105. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  106. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +107 -0
  107. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  108. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  109. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  110. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  111. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  112. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  113. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  114. data/lib/git/commands/config_option_syntax.rb +56 -0
  115. data/lib/git/commands/describe.rb +155 -0
  116. data/lib/git/commands/diff.rb +656 -0
  117. data/lib/git/commands/diff_files.rb +518 -0
  118. data/lib/git/commands/diff_index.rb +496 -0
  119. data/lib/git/commands/fetch.rb +352 -0
  120. data/lib/git/commands/fsck.rb +136 -0
  121. data/lib/git/commands/gc.rb +132 -0
  122. data/lib/git/commands/grep.rb +338 -0
  123. data/lib/git/commands/init.rb +99 -0
  124. data/lib/git/commands/log.rb +632 -0
  125. data/lib/git/commands/ls_files.rb +191 -0
  126. data/lib/git/commands/ls_remote.rb +155 -0
  127. data/lib/git/commands/ls_tree.rb +131 -0
  128. data/lib/git/commands/maintenance/register.rb +75 -0
  129. data/lib/git/commands/maintenance/run.rb +104 -0
  130. data/lib/git/commands/maintenance/start.rb +66 -0
  131. data/lib/git/commands/maintenance/stop.rb +55 -0
  132. data/lib/git/commands/maintenance/unregister.rb +79 -0
  133. data/lib/git/commands/maintenance.rb +31 -0
  134. data/lib/git/commands/merge/abort.rb +44 -0
  135. data/lib/git/commands/merge/continue.rb +44 -0
  136. data/lib/git/commands/merge/quit.rb +46 -0
  137. data/lib/git/commands/merge/start.rb +245 -0
  138. data/lib/git/commands/merge.rb +28 -0
  139. data/lib/git/commands/merge_base.rb +86 -0
  140. data/lib/git/commands/mv.rb +77 -0
  141. data/lib/git/commands/name_rev.rb +114 -0
  142. data/lib/git/commands/pull.rb +377 -0
  143. data/lib/git/commands/push.rb +246 -0
  144. data/lib/git/commands/read_tree.rb +149 -0
  145. data/lib/git/commands/remote/add.rb +91 -0
  146. data/lib/git/commands/remote/get_url.rb +66 -0
  147. data/lib/git/commands/remote/list.rb +54 -0
  148. data/lib/git/commands/remote/prune.rb +61 -0
  149. data/lib/git/commands/remote/remove.rb +52 -0
  150. data/lib/git/commands/remote/rename.rb +69 -0
  151. data/lib/git/commands/remote/set_branches.rb +63 -0
  152. data/lib/git/commands/remote/set_head.rb +82 -0
  153. data/lib/git/commands/remote/set_url.rb +71 -0
  154. data/lib/git/commands/remote/set_url_add.rb +61 -0
  155. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  156. data/lib/git/commands/remote/show.rb +71 -0
  157. data/lib/git/commands/remote/update.rb +72 -0
  158. data/lib/git/commands/remote.rb +42 -0
  159. data/lib/git/commands/repack.rb +277 -0
  160. data/lib/git/commands/reset.rb +147 -0
  161. data/lib/git/commands/rev_parse.rb +297 -0
  162. data/lib/git/commands/revert/abort.rb +45 -0
  163. data/lib/git/commands/revert/continue.rb +57 -0
  164. data/lib/git/commands/revert/quit.rb +47 -0
  165. data/lib/git/commands/revert/skip.rb +44 -0
  166. data/lib/git/commands/revert/start.rb +153 -0
  167. data/lib/git/commands/revert.rb +29 -0
  168. data/lib/git/commands/rm.rb +114 -0
  169. data/lib/git/commands/show.rb +632 -0
  170. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  171. data/lib/git/commands/show_ref/exists.rb +78 -0
  172. data/lib/git/commands/show_ref/list.rb +145 -0
  173. data/lib/git/commands/show_ref/verify.rb +120 -0
  174. data/lib/git/commands/show_ref.rb +42 -0
  175. data/lib/git/commands/stash/apply.rb +75 -0
  176. data/lib/git/commands/stash/branch.rb +65 -0
  177. data/lib/git/commands/stash/clear.rb +41 -0
  178. data/lib/git/commands/stash/create.rb +58 -0
  179. data/lib/git/commands/stash/drop.rb +67 -0
  180. data/lib/git/commands/stash/list.rb +39 -0
  181. data/lib/git/commands/stash/pop.rb +78 -0
  182. data/lib/git/commands/stash/push.rb +103 -0
  183. data/lib/git/commands/stash/show.rb +149 -0
  184. data/lib/git/commands/stash/store.rb +63 -0
  185. data/lib/git/commands/stash.rb +38 -0
  186. data/lib/git/commands/status.rb +169 -0
  187. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  188. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  189. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  190. data/lib/git/commands/symbolic_ref.rb +38 -0
  191. data/lib/git/commands/tag/create.rb +139 -0
  192. data/lib/git/commands/tag/delete.rb +55 -0
  193. data/lib/git/commands/tag/list.rb +143 -0
  194. data/lib/git/commands/tag/verify.rb +71 -0
  195. data/lib/git/commands/tag.rb +26 -0
  196. data/lib/git/commands/update_ref/batch.rb +140 -0
  197. data/lib/git/commands/update_ref/delete.rb +92 -0
  198. data/lib/git/commands/update_ref/update.rb +106 -0
  199. data/lib/git/commands/update_ref.rb +42 -0
  200. data/lib/git/commands/version.rb +57 -0
  201. data/lib/git/commands/worktree/add.rb +140 -0
  202. data/lib/git/commands/worktree/list.rb +64 -0
  203. data/lib/git/commands/worktree/lock.rb +58 -0
  204. data/lib/git/commands/worktree/management_base.rb +51 -0
  205. data/lib/git/commands/worktree/move.rb +66 -0
  206. data/lib/git/commands/worktree/prune.rb +67 -0
  207. data/lib/git/commands/worktree/remove.rb +63 -0
  208. data/lib/git/commands/worktree/repair.rb +76 -0
  209. data/lib/git/commands/worktree/unlock.rb +47 -0
  210. data/lib/git/commands/worktree.rb +43 -0
  211. data/lib/git/commands/write_tree.rb +68 -0
  212. data/lib/git/commands.rb +87 -0
  213. data/lib/git/config.rb +17 -0
  214. data/lib/git/config_entry_info.rb +106 -0
  215. data/lib/git/configuring.rb +665 -0
  216. data/lib/git/deprecation.rb +9 -0
  217. data/lib/git/detached_head_info.rb +54 -0
  218. data/lib/git/diff.rb +293 -7
  219. data/lib/git/diff_file_numstat_info.rb +29 -0
  220. data/lib/git/diff_file_patch_info.rb +134 -0
  221. data/lib/git/diff_file_raw_info.rb +127 -0
  222. data/lib/git/diff_info.rb +169 -0
  223. data/lib/git/diff_path_status.rb +67 -19
  224. data/lib/git/diff_result.rb +32 -0
  225. data/lib/git/diff_stats.rb +51 -14
  226. data/lib/git/dirstat_info.rb +86 -0
  227. data/lib/git/errors.rb +65 -2
  228. data/lib/git/execution_context/global.rb +31 -0
  229. data/lib/git/execution_context/repository.rb +136 -0
  230. data/lib/git/execution_context.rb +501 -0
  231. data/lib/git/file_ref.rb +74 -0
  232. data/lib/git/fsck_object.rb +9 -9
  233. data/lib/git/fsck_result.rb +1 -1
  234. data/lib/git/log.rb +9 -2
  235. data/lib/git/object.rb +90 -27
  236. data/lib/git/parsers/branch.rb +224 -0
  237. data/lib/git/parsers/cat_file.rb +111 -0
  238. data/lib/git/parsers/config_entry.rb +110 -0
  239. data/lib/git/parsers/diff.rb +585 -0
  240. data/lib/git/parsers/fsck.rb +133 -0
  241. data/lib/git/parsers/grep.rb +42 -0
  242. data/lib/git/parsers/ls_remote.rb +79 -0
  243. data/lib/git/parsers/ls_tree.rb +58 -0
  244. data/lib/git/parsers/stash.rb +208 -0
  245. data/lib/git/parsers/tag.rb +257 -0
  246. data/lib/git/remote.rb +120 -9
  247. data/lib/git/repository/branching.rb +743 -0
  248. data/lib/git/repository/committing.rb +187 -0
  249. data/lib/git/repository/configuring.rb +351 -0
  250. data/lib/git/repository/context_helpers.rb +264 -0
  251. data/lib/git/repository/diffing.rb +775 -0
  252. data/lib/git/repository/factories.rb +682 -0
  253. data/lib/git/repository/inspecting.rb +252 -0
  254. data/lib/git/repository/logging.rb +247 -0
  255. data/lib/git/repository/maintenance.rb +65 -0
  256. data/lib/git/repository/merging.rb +357 -0
  257. data/lib/git/repository/object_operations.rb +1199 -0
  258. data/lib/git/repository/path_resolver.rb +207 -0
  259. data/lib/git/repository/remote_operations.rb +898 -0
  260. data/lib/git/repository/shared_private.rb +51 -0
  261. data/lib/git/repository/staging.rb +554 -0
  262. data/lib/git/repository/stashing.rb +143 -0
  263. data/lib/git/repository/status_operations.rb +201 -0
  264. data/lib/git/repository/worktree_operations.rb +159 -0
  265. data/lib/git/repository.rb +203 -1
  266. data/lib/git/stash.rb +78 -4
  267. data/lib/git/stash_info.rb +104 -0
  268. data/lib/git/stashes.rb +125 -13
  269. data/lib/git/status.rb +215 -18
  270. data/lib/git/tag_delete_failure.rb +31 -0
  271. data/lib/git/tag_delete_result.rb +63 -0
  272. data/lib/git/tag_info.rb +105 -0
  273. data/lib/git/version.rb +109 -2
  274. data/lib/git/version_constraint.rb +81 -0
  275. data/lib/git/worktree.rb +108 -6
  276. data/lib/git/worktrees.rb +94 -7
  277. data/lib/git.rb +260 -61
  278. data/redesign/1_architecture_existing.md +54 -18
  279. data/redesign/2_architecture_redesign.md +365 -46
  280. data/redesign/3_architecture_implementation.md +1497 -63
  281. data/redesign/Phase 4 - Step A.md +360 -0
  282. data/redesign/beta_release.md +107 -0
  283. data/redesign/c1c2_audit.md +566 -0
  284. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  285. data/redesign/config_design.rb +501 -0
  286. data/tasks/gem_tasks.rake +4 -0
  287. data/tasks/npm_tasks.rake +7 -0
  288. data/tasks/rspec.rake +48 -0
  289. data/tasks/test.rake +13 -1
  290. data/tasks/yard.rake +34 -7
  291. metadata +365 -22
  292. data/lib/git/base.rb +0 -1128
  293. data/lib/git/index.rb +0 -6
  294. data/lib/git/lib.rb +0 -2277
  295. data/lib/git/path.rb +0 -38
  296. data/lib/git/working_directory.rb +0 -6
  297. /data/{release-please-config.json → .release-please-config.json} +0 -0
@@ -0,0 +1,898 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/config_option_syntax'
4
+ require 'git/commands/fetch'
5
+ require 'git/commands/ls_remote'
6
+ require 'git/commands/pull'
7
+ require 'git/commands/push'
8
+ require 'git/commands/remote'
9
+ require 'git/parsers/ls_remote'
10
+ require 'git/remote'
11
+
12
+ require 'git/repository/shared_private'
13
+
14
+ module Git
15
+ class Repository
16
+ # Mixin that adds remote operation facade methods to {Git::Repository}
17
+ #
18
+ # Included by {Git::Repository}.
19
+ #
20
+ # @api public
21
+ #
22
+ module RemoteOperations # rubocop:disable Metrics/ModuleLength
23
+ # Key normalizations for {#fetch} options
24
+ #
25
+ # Maps dash-style option keys (which the 4.x `Git::Lib#fetch` accepted)
26
+ # to their canonical underscore-style equivalents.
27
+ #
28
+ # @return [Hash{Symbol => Symbol}]
29
+ #
30
+ # @api private
31
+ #
32
+ FETCH_KEY_NORMALIZATIONS = { 'update-head-ok': :update_head_ok, 'prune-tags': :prune_tags }.freeze
33
+ private_constant :FETCH_KEY_NORMALIZATIONS
34
+
35
+ # Option keys accepted by {#fetch}
36
+ #
37
+ # Derived from the 4.x `FETCH_OPTION_MAP` in `Git::Lib`.
38
+ #
39
+ # @return [Array<Symbol>]
40
+ #
41
+ # @api private
42
+ #
43
+ FETCH_ALLOWED_OPTS = %i[all tags t prune p prune_tags P force f update_head_ok u unshallow depth ref].freeze
44
+ private_constant :FETCH_ALLOWED_OPTS
45
+
46
+ # Download objects and refs from a remote repository
47
+ #
48
+ # Fetches branches and/or tags from one or more other repositories, along
49
+ # with the objects necessary to complete their histories. The local
50
+ # tracking references are updated but the working directory is not
51
+ # modified.
52
+ #
53
+ # @example Fetch from the default remote
54
+ # repo.fetch
55
+ #
56
+ # @example Fetch from a named remote
57
+ # repo.fetch('upstream')
58
+ #
59
+ # @example Fetch all remotes at once
60
+ # repo.fetch(all: true)
61
+ #
62
+ # @example Fetch and prune deleted remote branches
63
+ # repo.fetch('origin', prune: true)
64
+ #
65
+ # @example Fetch a specific refspec
66
+ # repo.fetch('origin', ref: 'refs/heads/main:refs/remotes/origin/main')
67
+ #
68
+ # @example Fetch multiple refspecs
69
+ # repo.fetch('origin', ref: ['refs/heads/main', 'refs/heads/develop'])
70
+ #
71
+ # @example Fetch and include all tags
72
+ # repo.fetch('origin', tags: true)
73
+ #
74
+ # @overload fetch(remote = 'origin', opts = {})
75
+ #
76
+ # @param remote [String, Hash, nil] the remote name or URL to fetch from
77
+ #
78
+ # When a Hash is given it is treated as `opts` and `remote` defaults to
79
+ # `nil` (which omits the remote positional argument and lets git use the
80
+ # configured default).
81
+ #
82
+ # @param opts [Hash] options for the fetch command
83
+ #
84
+ # @option opts [Boolean, nil] :all (nil) fetch from all configured remotes
85
+ # (`--all`)
86
+ #
87
+ # @option opts [Boolean, nil] :tags (nil) fetch all tags from the remote
88
+ # (`--tags`)
89
+ #
90
+ # Alias: `:t`
91
+ #
92
+ # @option opts [Boolean, nil] :prune (nil) remove remote-tracking references
93
+ # that no longer exist on the remote (`--prune`)
94
+ #
95
+ # Alias: `:p`
96
+ #
97
+ # @option opts [Boolean, nil] :prune_tags (nil) remove local tags that no
98
+ # longer exist on the remote (`--prune-tags`)
99
+ #
100
+ # Alias: `:P`. The legacy dash-style key `:'prune-tags'` is also accepted
101
+ # and normalized automatically.
102
+ #
103
+ # @option opts [Boolean, nil] :force (nil) override the fast-forward check
104
+ # when using explicit refspecs (`--force`)
105
+ #
106
+ # Alias: `:f`
107
+ #
108
+ # @option opts [Boolean, nil] :update_head_ok (nil) allow `git fetch` to
109
+ # update the branch pointed to by `HEAD` (`--update-head-ok`)
110
+ #
111
+ # Alias: `:u`. The legacy dash-style key `:'update-head-ok'` is also
112
+ # accepted and normalized automatically.
113
+ #
114
+ # @option opts [Boolean, nil] :unshallow (nil) convert a shallow clone into a
115
+ # full repository (`--unshallow`)
116
+ #
117
+ # @option opts [String, Integer] :depth (nil) limit history to N commits
118
+ # from each branch tip (`--depth=N`)
119
+ #
120
+ # @option opts [String, Array<String>] :ref (nil) one or more refspecs to
121
+ # fetch; forwarded as positional arguments after the remote name. An
122
+ # explicit `remote` is required when `:ref` is given.
123
+ #
124
+ # @return [String] the merged stdout from the fetch command
125
+ #
126
+ # @raise [ArgumentError] when unsupported option keys are provided or `:ref`
127
+ # is supplied without an explicit remote
128
+ #
129
+ # @raise [Git::FailedError] when git exits with a non-zero status
130
+ #
131
+ def fetch(remote = 'origin', opts = {})
132
+ remote, opts = Private.resolve_fetch_target(remote, opts)
133
+
134
+ opts = Private.normalize_fetch_keys(opts)
135
+ SharedPrivate.assert_valid_opts!(FETCH_ALLOWED_OPTS, **opts)
136
+
137
+ opts = opts.dup
138
+ refspecs = Array(opts.delete(:ref)).compact
139
+ positionals = [*([remote] if remote), *refspecs]
140
+
141
+ Git::Commands::Fetch.new(@execution_context).call(*positionals, **opts, merge: true).stdout
142
+ end
143
+
144
+ # Option keys accepted by {#pull}
145
+ #
146
+ # Derived from the 4.x `PULL_OPTION_MAP` in `Git::Lib`.
147
+ #
148
+ # @return [Array<Symbol>]
149
+ #
150
+ # @api private
151
+ #
152
+ PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
153
+ private_constant :PULL_ALLOWED_OPTS
154
+
155
+ # Incorporate changes from a remote repository into the current branch
156
+ #
157
+ # Fetches from the given remote and merges into the current branch. In its
158
+ # default mode, `git pull` is shorthand for `git fetch` followed by
159
+ # `git merge FETCH_HEAD`. The merge editor is suppressed (`--no-edit`) and
160
+ # progress output is silenced (`--no-progress`) by default.
161
+ #
162
+ # @example Pull from the default remote and branch
163
+ # repo.pull
164
+ #
165
+ # @example Pull from a named remote
166
+ # repo.pull('upstream')
167
+ #
168
+ # @example Pull a specific branch from a remote
169
+ # repo.pull('origin', 'main')
170
+ #
171
+ # @example Pull allowing unrelated histories
172
+ # repo.pull('origin', 'main', allow_unrelated_histories: true)
173
+ #
174
+ # @overload pull(remote = nil, branch = nil, opts = {})
175
+ #
176
+ # @param remote [String, nil] the remote name or URL to pull from
177
+ #
178
+ # When nil, git uses the tracking remote for the current branch.
179
+ #
180
+ # @param branch [String, nil] the remote branch name to pull
181
+ #
182
+ # When nil, git uses the tracking branch for the current branch.
183
+ # A branch may not be specified without also specifying a remote.
184
+ #
185
+ # @param opts [Hash] options for the pull command
186
+ #
187
+ # @option opts [Boolean, nil] :allow_unrelated_histories (nil) allow merging
188
+ # histories that do not share a common ancestor
189
+ # (`--allow-unrelated-histories`)
190
+ #
191
+ # @return [String] the stdout from the pull command
192
+ #
193
+ # @raise [ArgumentError] when a branch is given without a remote, or when
194
+ # unsupported option keys are provided
195
+ #
196
+ # @raise [Git::FailedError] when git exits with a non-zero status
197
+ #
198
+ def pull(remote = nil, branch = nil, opts = {})
199
+ raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?
200
+
201
+ SharedPrivate.assert_valid_opts!(PULL_ALLOWED_OPTS, **opts)
202
+ positional_args = [remote, branch].compact
203
+ Git::Commands::Pull
204
+ .new(@execution_context)
205
+ .call(*positional_args, no_edit: true, no_progress: true, **opts)
206
+ .stdout
207
+ end
208
+
209
+ # Option keys accepted by {#push}
210
+ #
211
+ # Derived from the 4.x `PUSH_OPTION_MAP` in `Git::Lib`.
212
+ #
213
+ # @return [Array<Symbol>]
214
+ #
215
+ # @api private
216
+ #
217
+ PUSH_ALLOWED_OPTS = %i[mirror delete force f push_option all tags].freeze
218
+ private_constant :PUSH_ALLOWED_OPTS
219
+
220
+ # Push refs to a remote repository
221
+ #
222
+ # @example Push using the current branch's default remote and push configuration
223
+ # repo.push
224
+ #
225
+ # @example Push to a named remote
226
+ # repo.push('origin')
227
+ #
228
+ # @example Force-push the current branch to a named remote
229
+ # repo.push('origin', force: true)
230
+ #
231
+ # @example Push a specific branch to a named remote
232
+ # repo.push('origin', 'main')
233
+ #
234
+ # @example Push a branch and all tags to a named remote
235
+ # repo.push('origin', 'main', tags: true)
236
+ #
237
+ # @example Push all branches to a named remote
238
+ # repo.push('origin', all: true)
239
+ #
240
+ # @example Mirror all refs to a named remote
241
+ # repo.push('origin', mirror: true)
242
+ #
243
+ # @overload push(options = {})
244
+ # Push using the current branch's default remote and push configuration
245
+ #
246
+ # @param options [Hash] push options (see option list below)
247
+ #
248
+ # @option options [Boolean, nil] :all (nil) push all branches (`--all`)
249
+ #
250
+ # @option options [Boolean, nil] :mirror (nil) push all refs under
251
+ # `refs/` to the remote (`--mirror`)
252
+ #
253
+ # When `:tags` is also given, the separate tags push is suppressed.
254
+ #
255
+ # @option options [Boolean, nil] :tags (nil) push all refs under
256
+ # `refs/tags/` in a second `git push` invocation (`--tags`)
257
+ #
258
+ # When `:mirror` is also given, the tags push is suppressed because
259
+ # `--mirror` already includes tags.
260
+ #
261
+ # @option options [Boolean, nil] :force (nil) force updates,
262
+ # overriding the fast-forward check (`--force`)
263
+ #
264
+ # Alias: `:f`
265
+ #
266
+ # @option options [Boolean, nil] :delete (nil) delete the named refs
267
+ # from the remote (`--delete`)
268
+ #
269
+ # @option options [String, Array<String>] :push_option (nil) one or
270
+ # more server-side push option values (`--push-option=<value>`,
271
+ # repeatable)
272
+ #
273
+ # @return [String] the stdout from the push command
274
+ #
275
+ # @overload push(remote, options = {})
276
+ # Push to the given remote using the current branch's default push configuration
277
+ #
278
+ # @param remote [String] the remote name or URL to push to
279
+ #
280
+ # @param options [Hash] push options (see option list below)
281
+ #
282
+ # @option options [Boolean, nil] :all (nil) push all branches (`--all`)
283
+ #
284
+ # @option options [Boolean, nil] :mirror (nil) push all refs under
285
+ # `refs/` to the remote (`--mirror`)
286
+ #
287
+ # When `:tags` is also given, the separate tags push is suppressed.
288
+ #
289
+ # @option options [Boolean, nil] :tags (nil) push all refs under
290
+ # `refs/tags/` in a second `git push` invocation (`--tags`)
291
+ #
292
+ # When `:mirror` is also given, the tags push is suppressed because
293
+ # `--mirror` already includes tags.
294
+ #
295
+ # @option options [Boolean, nil] :force (nil) force updates,
296
+ # overriding the fast-forward check (`--force`)
297
+ #
298
+ # Alias: `:f`
299
+ #
300
+ # @option options [Boolean, nil] :delete (nil) delete the named refs
301
+ # from the remote (`--delete`)
302
+ #
303
+ # @option options [String, Array<String>] :push_option (nil) one or
304
+ # more server-side push option values (`--push-option=<value>`,
305
+ # repeatable)
306
+ #
307
+ # @return [String] the stdout from the push command
308
+ #
309
+ # @overload push(remote, branch, options = {})
310
+ # Push a branch or refspec to the given remote
311
+ #
312
+ # @param remote [String] the remote name or URL to push to
313
+ #
314
+ # @param branch [String] the branch name or refspec to push
315
+ #
316
+ # @param options [Hash] push options (see option list below)
317
+ #
318
+ # @option options [Boolean, nil] :all (nil) push all branches (`--all`)
319
+ #
320
+ # @option options [Boolean, nil] :mirror (nil) push all refs under
321
+ # `refs/` to the remote (`--mirror`)
322
+ #
323
+ # When `:tags` is also given, the separate tags push is suppressed.
324
+ #
325
+ # @option options [Boolean, nil] :tags (nil) push all refs under
326
+ # `refs/tags/` in a second `git push` invocation (`--tags`)
327
+ #
328
+ # When `:mirror` is also given, the tags push is suppressed because
329
+ # `--mirror` already includes tags.
330
+ #
331
+ # @option options [Boolean, nil] :force (nil) force updates,
332
+ # overriding the fast-forward check (`--force`)
333
+ #
334
+ # Alias: `:f`
335
+ #
336
+ # @option options [Boolean, nil] :delete (nil) delete the named refs
337
+ # from the remote (`--delete`)
338
+ #
339
+ # @option options [String, Array<String>] :push_option (nil) one or
340
+ # more server-side push option values (`--push-option=<value>`,
341
+ # repeatable)
342
+ #
343
+ # @return [String] the stdout from the push command
344
+ #
345
+ # @raise [ArgumentError] if `remote` is nil when `branch` is given
346
+ #
347
+ # @overload push(remote, branch, tags)
348
+ # Backward-compatible shorthand for `push(remote, branch, tags: tags)`
349
+ #
350
+ # @param remote [String] the remote name or URL to push to
351
+ #
352
+ # @param branch [String] the branch name or refspec to push
353
+ #
354
+ # @param tags [Boolean] whether to push all tags; equivalent to `tags: tags`
355
+ #
356
+ # @return [String] the stdout from the push command
357
+ #
358
+ # @raise [ArgumentError] if a branch is given and remote is nil
359
+ #
360
+ # @raise [ArgumentError] when unsupported option keys are provided
361
+ #
362
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
363
+ #
364
+ def push(remote = nil, branch = nil, opts = nil)
365
+ remote, branch, opts = Private.normalize_push_args(remote, branch, opts)
366
+ SharedPrivate.assert_valid_opts!(PUSH_ALLOWED_OPTS, **opts)
367
+ raise ArgumentError, 'remote is required if branch is specified' if !remote && branch
368
+
369
+ first_result = Private.push_refs(@execution_context, remote, branch, opts)
370
+ return first_result.stdout unless Private.push_tags_separately?(opts)
371
+
372
+ Private.push_tags(@execution_context, remote, opts).stdout
373
+ end
374
+
375
+ # Option keys accepted by {#remote_add}
376
+ #
377
+ # Derived from the 4.x `REMOTE_ADD_OPTION_MAP` in `Git::Lib`.
378
+ REMOTE_ADD_ALLOWED_OPTS = %i[fetch track].freeze
379
+ private_constant :REMOTE_ADD_ALLOWED_OPTS
380
+
381
+ # Register a new remote in the local repository
382
+ #
383
+ # Associates `name` with `url` and optionally fetches immediately or
384
+ # configures which branches are tracked.
385
+ #
386
+ # @example Add a remote
387
+ # repo.remote_add('upstream', 'https://github.com/user/repo.git')
388
+ #
389
+ # @example Add a remote and fetch immediately
390
+ # repo.remote_add('upstream', 'https://github.com/user/repo.git', fetch: true)
391
+ #
392
+ # @example Add a remote tracking a specific branch
393
+ # repo.remote_add('upstream', 'https://github.com/user/repo.git', track: 'main')
394
+ #
395
+ # @param name [String] the name for the new remote
396
+ #
397
+ # @param url [String, Git::Repository] the URL of the remote repository
398
+ #
399
+ # A {Git::Repository} instance is accepted for local references and converted
400
+ # to `url.repo.to_s`.
401
+ #
402
+ # @param opts [Hash] options for adding the remote
403
+ #
404
+ # @option opts [Boolean, nil] :fetch (nil) fetch from the remote immediately
405
+ # after adding it (`-f`)
406
+ #
407
+ # The deprecated alias `:with_fetch` is accepted and normalized
408
+ # automatically.
409
+ #
410
+ # @option opts [String, nil] :track (nil) track only the given branch during
411
+ # fetch (`-t`)
412
+ #
413
+ # @return [Git::Remote] the newly added remote
414
+ #
415
+ # @raise [ArgumentError] when unsupported option keys are provided
416
+ #
417
+ # @raise [Git::FailedError] when git exits with a non-zero status
418
+ #
419
+ def remote_add(name, url, opts = {})
420
+ url = url.repo.to_s if url.is_a?(Git::Repository)
421
+ opts = Private.normalize_add_remote_keys(opts)
422
+ SharedPrivate.assert_valid_opts!(REMOTE_ADD_ALLOWED_OPTS, **opts)
423
+ Git::Commands::Remote::Add.new(@execution_context).call(name, url, **opts)
424
+
425
+ Git::Remote.new(self, name)
426
+ end
427
+
428
+ # @deprecated Use {#remote_add} instead.
429
+ #
430
+ # @param name [String] the name for the new remote
431
+ #
432
+ # @param url [String, Git::Repository] the URL of the remote repository
433
+ #
434
+ # A {Git::Repository} instance is accepted for local references and converted
435
+ # to `url.repo.to_s`.
436
+ #
437
+ # @param opts [Hash] options for adding the remote
438
+ #
439
+ # @option opts [Boolean, nil] :fetch (nil) fetch from the remote immediately
440
+ # after adding it (`-f`)
441
+ #
442
+ # The deprecated alias `:with_fetch` is accepted and normalized
443
+ # automatically.
444
+ #
445
+ # @option opts [String, nil] :track (nil) track only the given branch during
446
+ # fetch (`-t`)
447
+ #
448
+ # @return [Git::Remote] the newly added remote
449
+ #
450
+ # @raise [ArgumentError] when unsupported option keys are provided
451
+ #
452
+ # @raise [Git::FailedError] when git exits with a non-zero status
453
+ #
454
+ def add_remote(name, url, opts = {})
455
+ Git::Deprecation.warn(
456
+ 'Git::Repository#add_remote is deprecated and will be removed in v6.0.0. ' \
457
+ 'Use Git::Repository#remote_add instead.'
458
+ )
459
+ remote_add(name, url, opts)
460
+ end
461
+
462
+ # Removes a remote from this repository
463
+ #
464
+ # Deletes the remote named `name` along with its associated configuration,
465
+ # tracking references, and remote-tracking branches.
466
+ #
467
+ # @example Remove a remote named 'upstream'
468
+ # repo.remote_remove('upstream')
469
+ #
470
+ # @param name [String] the name of the remote to remove
471
+ #
472
+ # @return [Git::CommandLineResult] the result of calling `git remote remove`
473
+ #
474
+ # @raise [Git::FailedError] when git exits with a non-zero status
475
+ #
476
+ def remote_remove(name)
477
+ Git::Commands::Remote::Remove.new(@execution_context).call(name)
478
+ end
479
+
480
+ # @deprecated Use {#remote_remove} instead.
481
+ #
482
+ # @param name [String] the name of the remote to remove
483
+ #
484
+ # @return [Git::CommandLineResult] the result of calling `git remote remove`
485
+ #
486
+ # @raise [Git::FailedError] when git exits with a non-zero status
487
+ #
488
+ def remove_remote(name)
489
+ Git::Deprecation.warn(
490
+ 'Git::Repository#remove_remote is deprecated and will be removed in v6.0.0. ' \
491
+ 'Use Git::Repository#remote_remove instead.'
492
+ )
493
+ remote_remove(name)
494
+ end
495
+
496
+ # Sets the URL for an existing remote
497
+ #
498
+ # Replaces the fetch URL configured for the remote named `name`.
499
+ #
500
+ # @example Set the URL for a remote
501
+ # repo.remote_set_url('origin', 'https://github.com/user/repo.git')
502
+ #
503
+ # @example Set the URL from a local repository reference
504
+ # source = Git.open('/path/to/source')
505
+ # repo.remote_set_url('origin', source)
506
+ #
507
+ # @param name [String] the name of the remote to update
508
+ #
509
+ # @param url [String, Git::Repository] the new URL for the remote
510
+ #
511
+ # A {Git::Repository} instance is accepted for local references and converted
512
+ # to `url.repo.to_s`.
513
+ #
514
+ # @return [Git::Remote] the updated remote
515
+ #
516
+ # @raise [Git::FailedError] when git exits with a non-zero status
517
+ #
518
+ def remote_set_url(name, url)
519
+ url = url.repo.to_s if url.is_a?(Git::Repository)
520
+ Git::Commands::Remote::SetUrl.new(@execution_context).call(name, url)
521
+
522
+ Git::Remote.new(self, name)
523
+ end
524
+
525
+ # @deprecated Use {#remote_set_url} instead.
526
+ #
527
+ # @param name [String] the name of the remote to update
528
+ #
529
+ # @param url [String, Git::Repository] the new URL for the remote
530
+ #
531
+ # A {Git::Repository} instance is accepted for local references and converted
532
+ # to `url.repo.to_s`.
533
+ #
534
+ # @return [Git::Remote] the updated remote
535
+ #
536
+ # @raise [Git::FailedError] when git exits with a non-zero status
537
+ #
538
+ def set_remote_url(name, url)
539
+ Git::Deprecation.warn(
540
+ 'Git::Repository#set_remote_url is deprecated and will be removed in v6.0.0. ' \
541
+ 'Use Git::Repository#remote_set_url instead.'
542
+ )
543
+ remote_set_url(name, url)
544
+ end
545
+
546
+ # Configures which branches are fetched for a remote
547
+ #
548
+ # Uses `git remote set-branches` to set or append fetch refspecs. When the
549
+ # `add:` option is `false`, the `--add` flag is not passed to the git
550
+ # command and the tracked branch list is replaced.
551
+ #
552
+ # @example Replace fetched branches with a single glob pattern
553
+ # repo.remote_set_branches('origin', 'feature/*')
554
+ #
555
+ # @example Append a glob pattern to existing fetched branches
556
+ # repo.remote_set_branches('origin', 'release/*', add: true)
557
+ #
558
+ # @example Configure multiple explicit branches
559
+ # repo.remote_set_branches('origin', 'main', 'development', 'hotfix')
560
+ #
561
+ # @param name [String] the remote name (for example, `"origin"`)
562
+ #
563
+ # @param branches [Array<String>] branch names or globs (for example, `'*'`)
564
+ #
565
+ # @param add [Boolean] when `true`, append to existing refspecs instead of
566
+ # replacing them
567
+ #
568
+ # @return [void]
569
+ #
570
+ # @raise [ArgumentError] when no branches are provided
571
+ #
572
+ # @raise [Git::FailedError] when git exits with a non-zero status
573
+ #
574
+ def remote_set_branches(name, *branches, add: false)
575
+ branch_list = branches.flatten
576
+ raise ArgumentError, 'branches are required' if branch_list.empty?
577
+
578
+ Git::Commands::Remote::SetBranches.new(@execution_context).call(name, *branch_list, add: add)
579
+
580
+ nil
581
+ end
582
+
583
+ # Return the git configuration entries for a named remote
584
+ #
585
+ # Reads `git config --list` and returns all entries whose keys begin with
586
+ # `remote.<name>.`, with the `remote.<name>.` prefix stripped. This
587
+ # typically yields at least `"url"` and `"fetch"` for a configured remote.
588
+ #
589
+ # @example Retrieve the config for the 'origin' remote
590
+ # repo.config_remote('origin')
591
+ # #=> {
592
+ # # 'url' => 'https://github.com/user/repo.git',
593
+ # # 'fetch' => '+refs/heads/*:refs/remotes/origin/*'
594
+ # # }
595
+ #
596
+ # @param name [String] the name of the remote (e.g. `"origin"`)
597
+ #
598
+ # @return [Hash{String => String}] configuration entries for the remote,
599
+ # keyed without the `remote.<name>.` prefix
600
+ #
601
+ # Returns an empty hash when no entries are found.
602
+ #
603
+ # @raise [Git::FailedError] when git exits with a non-zero status
604
+ #
605
+ def config_remote(name)
606
+ prefix = "remote.#{name}."
607
+ Private.config_list(@execution_context).each_with_object({}) do |(key, value), hsh|
608
+ hsh[key.delete_prefix(prefix)] = value if key.start_with?(prefix)
609
+ end
610
+ end
611
+
612
+ # Returns a {Git::Remote} object for the named remote
613
+ #
614
+ # @example Get the default 'origin' remote
615
+ # repo.remote #=> #<Git::Remote 'origin'>
616
+ #
617
+ # @example Get a named remote
618
+ # repo.remote('upstream') #=> #<Git::Remote 'upstream'>
619
+ #
620
+ # @param name [String] the remote name (defaults to `'origin'`)
621
+ #
622
+ # @return [Git::Remote] the remote object
623
+ #
624
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
625
+ #
626
+ def remote(name = 'origin')
627
+ Git::Remote.new(self, name)
628
+ end
629
+
630
+ # Returns all configured remotes as {Git::Remote} objects
631
+ #
632
+ # @example List all remotes
633
+ # repo.remotes #=> [#<Git::Remote 'origin'>, #<Git::Remote 'upstream'>]
634
+ #
635
+ # @return [Array<Git::Remote>] one {Git::Remote} for each configured remote
636
+ #
637
+ # Returns an empty array when no remotes are configured.
638
+ #
639
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
640
+ #
641
+ def remotes
642
+ result = Git::Commands::Remote::List.new(@execution_context).call
643
+ result.stdout.split("\n").map { |name| Git::Remote.new(self, name) }
644
+ end
645
+
646
+ # Option keys accepted by {#ls_remote}
647
+ #
648
+ # @return [Array<Symbol>]
649
+ #
650
+ # @api private
651
+ #
652
+ LS_REMOTE_ALLOWED_OPTS = %i[
653
+ branches b heads h tags t refs upload_pack quiet q exit_code sort server_option o timeout
654
+ ].freeze
655
+ private_constant :LS_REMOTE_ALLOWED_OPTS
656
+
657
+ # List references available in a remote repository
658
+ #
659
+ # Queries a remote for its available refs and returns a structured Hash
660
+ # mapping ref types to name/sha pairs. The remote is contacted but no local
661
+ # objects are created or updated.
662
+ #
663
+ # @example List all refs from the local repository
664
+ # repo.ls_remote
665
+ # # => {"head"=>{ref: "HEAD", sha: "abc123"},
666
+ # # "branches"=>{"main"=>{ref: "refs", sha: "abc123"}}}
667
+ #
668
+ # @note The `:ref` value in each pair is only the **first path segment** of the
669
+ # full git ref (e.g. `"refs"` for `refs/heads/main`), not the complete ref
670
+ # path. This matches the behavior of 4.x. See
671
+ # [issue 1416](https://github.com/ruby-git/ruby-git/issues/1416) for the
672
+ # planned fix.
673
+ #
674
+ # @example List all refs from a named remote
675
+ # repo.ls_remote('origin')
676
+ # # => {"head"=>..., "branches"=>..., "tags"=>...}
677
+ #
678
+ # @example List only tags from a named remote
679
+ # repo.ls_remote('origin', tags: true)
680
+ # # => {"tags"=>{"v1.0"=>{ref: "refs", sha: "def456"}}}
681
+ #
682
+ # @param location [String, nil] the remote name or URL to query; defaults to
683
+ # `'.'` (the local repository) when nil
684
+ #
685
+ # @param opts [Hash] options for the ls-remote command
686
+ #
687
+ # @option opts [Boolean, nil] :branches (nil) limit output to refs under
688
+ # `refs/heads/`; alias: `:b`
689
+ #
690
+ # @option opts [Boolean, nil] :heads (nil) limit output to refs under
691
+ # `refs/heads/`; kept for backward compatibility; alias: `:h`
692
+ #
693
+ # @option opts [Boolean, nil] :tags (nil) limit output to refs under
694
+ # `refs/tags/`; alias: `:t`
695
+ #
696
+ # @option opts [Boolean, nil] :refs (nil) exclude peeled tags and pseudorefs
697
+ # like `HEAD` from the output
698
+ #
699
+ # @option opts [Numeric] :timeout (nil) execution timeout in seconds
700
+ #
701
+ # @return [Hash{String => Hash}] a Hash keyed by ref type (e.g. `"head"`,
702
+ # `"branches"`, `"tags"`; other git namespace segments may appear for
703
+ # non-standard refs); for named refs the value is a Hash keyed by ref name
704
+ # mapping to `{ ref: String, sha: String }`; for the `"head"` entry the value
705
+ # is `{ ref: String, sha: String }` directly
706
+ #
707
+ # @raise [ArgumentError] if unsupported options are provided
708
+ #
709
+ # @raise [Git::FailedError] if git exits outside the allowed range (exit code > 2)
710
+ #
711
+ def ls_remote(location = nil, opts = {})
712
+ SharedPrivate.assert_valid_opts!(LS_REMOTE_ALLOWED_OPTS, **opts)
713
+ repository = location || '.'
714
+ output_lines = Git::Commands::LsRemote.new(@execution_context).call(repository, **opts).stdout.split("\n")
715
+ Git::Parsers::LsRemote.parse_output(output_lines)
716
+ end
717
+
718
+ # Helpers private to the `RemoteOperations` topic module
719
+ #
720
+ # @api private
721
+ #
722
+ module Private
723
+ module_function
724
+
725
+ # Resolve the (remote, opts) pair for {#fetch}, supporting the hash-only form
726
+ #
727
+ # `fetch` may be called as `fetch(remote, opts)` or `fetch(opts)`. When a bare
728
+ # options hash is passed the remote is treated as nil. A `:ref` is only
729
+ # meaningful with an explicit remote, so requesting one without a remote (it
730
+ # would otherwise be silently promoted to the remote-name slot) is rejected.
731
+ #
732
+ # @param remote [String, Hash, nil] the remote name, or an options hash
733
+ # @param opts [Hash] the options hash when remote is given positionally
734
+ #
735
+ # @return [Array(String, Hash), Array(nil, Hash)] the resolved remote and opts
736
+ #
737
+ # @raise [ArgumentError] when :ref is supplied without an explicit remote
738
+ #
739
+ # @api private
740
+ #
741
+ def resolve_fetch_target(remote, opts)
742
+ if remote.is_a?(Hash)
743
+ opts = remote
744
+ remote = nil
745
+ end
746
+
747
+ raise ArgumentError, ':ref requires an explicit remote' if remote.nil? && opts.key?(:ref)
748
+
749
+ [remote, opts]
750
+ end
751
+
752
+ # Normalize dash-style option keys to their underscore equivalents
753
+ #
754
+ # Converts any key in {FETCH_KEY_NORMALIZATIONS} from its dash-style symbol
755
+ # form (e.g., `:'update-head-ok'`) to the canonical underscore-style form
756
+ # (e.g., `:update_head_ok`). Unrecognized keys are returned unchanged.
757
+ #
758
+ # @param opts [Hash] the raw options hash passed by the caller
759
+ #
760
+ # @return [Hash] a new hash with all applicable keys normalized
761
+ #
762
+ # @api private
763
+ #
764
+ def normalize_fetch_keys(opts)
765
+ opts.transform_keys do |k|
766
+ sym = k.is_a?(Symbol) ? k : k.to_sym
767
+ FETCH_KEY_NORMALIZATIONS.fetch(sym, sym)
768
+ end
769
+ end
770
+
771
+ # Normalize the flexible argument list accepted by {RemoteOperations#push}
772
+ #
773
+ # Handles three call forms:
774
+ # - `push(opts)` — Hash promoted from `remote` position
775
+ # - `push(remote, opts)` — Hash promoted from `branch` position
776
+ # - `push(remote, branch, true|false)` — Boolean `opts` converted to
777
+ # `{ tags: opts }` for backward compatibility
778
+ #
779
+ # @param remote [String, Hash, nil] remote name, URL, or opts hash
780
+ # @param branch [String, Hash, nil] branch/refspec, or opts hash
781
+ # @param opts [Hash, Boolean, nil] options hash or legacy Boolean shorthand
782
+ #
783
+ # @return [Array(String|nil, String|nil, Hash)] normalized [remote, branch, opts]
784
+ #
785
+ # @api private
786
+ #
787
+ def normalize_push_args(remote, branch, opts)
788
+ if branch.is_a?(Hash)
789
+ opts = branch
790
+ branch = nil
791
+ elsif remote.is_a?(Hash)
792
+ opts = remote
793
+ remote = nil
794
+ end
795
+
796
+ opts ||= {}
797
+
798
+ # Backwards compatibility for `push(remote, branch, true)` to push tags
799
+ # without requiring the caller to use keyword arguments
800
+
801
+ opts = { tags: opts } if [true, false].include?(opts)
802
+ [remote, branch, opts]
803
+ end
804
+
805
+ # Issue the refs push (first push when `:tags` is given separately)
806
+ #
807
+ # Strips `:tags` from the options so that only refs — not tags — are pushed
808
+ # in this first call. Tags are pushed in a separate call when
809
+ # {push_tags_separately?} is true.
810
+ #
811
+ # @param execution_context [Git::ExecutionContext::Repository] the repository execution context
812
+ # @param remote [String, nil] remote name or URL
813
+ # @param branch [String, nil] branch or refspec
814
+ # @param opts [Hash] push options (`:tags` key will be stripped)
815
+ #
816
+ # @return [Git::CommandLineResult]
817
+ #
818
+ # @api private
819
+ #
820
+ def push_refs(execution_context, remote, branch, opts)
821
+ positionals = [remote, branch].compact
822
+ Git::Commands::Push.new(execution_context).call(*positionals, **opts.except(:tags))
823
+ end
824
+
825
+ # Return true when tags must be pushed in a second separate invocation
826
+ #
827
+ # Tags are pushed separately when `:tags` is truthy AND `:mirror` is not set.
828
+ # When `:mirror` is set, the mirror push already includes all refs and tags,
829
+ # so a second tags-only call would be redundant.
830
+ #
831
+ # @param opts [Hash] the normalized push options
832
+ #
833
+ # @return [Boolean]
834
+ #
835
+ # @api private
836
+ #
837
+ def push_tags_separately?(opts)
838
+ opts[:tags] && !opts[:mirror]
839
+ end
840
+
841
+ # Issue the tags push (second push when `:tags` is requested without `:mirror`)
842
+ #
843
+ # @param execution_context [Git::ExecutionContext::Repository] the repository execution context
844
+ # @param remote [String, nil] remote name or URL
845
+ # @param opts [Hash] push options (`:tags` key included to emit `--tags`)
846
+ #
847
+ # @return [Git::CommandLineResult]
848
+ #
849
+ # @api private
850
+ #
851
+ def push_tags(execution_context, remote, opts)
852
+ Git::Commands::Push.new(execution_context).call(*[remote].compact, **opts)
853
+ end
854
+
855
+ # Normalize deprecated option keys for {#remote_add} to their canonical equivalents
856
+ #
857
+ # Renames the deprecated `:with_fetch` key to `:fetch`, removing it from
858
+ # the copy. When both keys are present, `:with_fetch` takes precedence.
859
+ #
860
+ # @param opts [Hash] the raw options hash passed by the caller
861
+ #
862
+ # @return [Hash] a new hash with all applicable keys normalized
863
+ #
864
+ # @api private
865
+ #
866
+ def normalize_add_remote_keys(opts)
867
+ normalized = opts.dup
868
+ normalized[:fetch] = normalized.delete(:with_fetch) if normalized.key?(:with_fetch)
869
+ normalized
870
+ end
871
+
872
+ # Retrieve all config entries as a flat hash
873
+ #
874
+ # Runs `git config --list` and parses each `key=value` line into a hash.
875
+ # When no value is present for a key, the value defaults to an empty string.
876
+ #
877
+ # @param execution_context [Git::ExecutionContext::Repository] the
878
+ # execution context for the repository
879
+ #
880
+ # @return [Hash{String => String}] all visible config entries, keyed by
881
+ # their full dotted key names
882
+ #
883
+ # For example, `"remote.origin.url"` is a valid key.
884
+ #
885
+ # @api private
886
+ #
887
+ def config_list(execution_context)
888
+ lines = Git::Commands::ConfigOptionSyntax::List.new(execution_context).call.stdout.split("\n")
889
+ lines.each_with_object({}) do |line, hsh|
890
+ key, value = line.split('=', 2)
891
+ hsh[key] = value || ''
892
+ end
893
+ end
894
+ end
895
+ private_constant :Private
896
+ end
897
+ end
898
+ end