git 1.19.1 → 5.5.0

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