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,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/gc'
4
+ require 'git/commands/repack'
5
+
6
+ module Git
7
+ class Repository
8
+ # Facade methods for repository maintenance and optimization operations
9
+ #
10
+ # These methods pack objects, compress history, prune unreachable objects, and
11
+ # otherwise keep the repository in good health.
12
+ #
13
+ # Included by {Git::Repository}.
14
+ #
15
+ # @api private
16
+ #
17
+ module Maintenance
18
+ # Repack loose objects into pack files
19
+ #
20
+ # Packs all unpacked objects and removes redundant pack files. This is
21
+ # equivalent to running `git repack -a -d`, which packs all objects into a
22
+ # single pack and deletes any packs that become redundant.
23
+ #
24
+ # This method uses the fixed options `a: true, d: true` (matching the 4.x
25
+ # behavior). No additional options are exposed.
26
+ #
27
+ # @example Repack the repository
28
+ # repo.repack
29
+ #
30
+ # @return [String] the stdout from `git repack`. Git writes all progress
31
+ # and summary output to stderr, so the returned string is typically empty.
32
+ # Returns `String` to match the 4.x public contract (`Git::Lib#command`
33
+ # returned `result.stdout`).
34
+ #
35
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
36
+ #
37
+ def repack
38
+ Git::Commands::Repack.new(@execution_context).call(a: true, d: true).stdout
39
+ end
40
+
41
+ # Run garbage collection to optimize and clean up the repository
42
+ #
43
+ # Runs `git gc` to perform housekeeping tasks including object compression,
44
+ # pruning of unreachable objects, and ref packing. This is equivalent to
45
+ # running `git gc --prune --aggressive --auto`.
46
+ #
47
+ # This method uses the fixed options `prune: true, aggressive: true, auto: true`
48
+ # (matching the 4.x behavior). No additional options are exposed.
49
+ #
50
+ # @example Run garbage collection
51
+ # repo.gc
52
+ #
53
+ # @return [String] the stdout from `git gc`. Git writes all progress
54
+ # and summary output to stderr, so the returned string is typically empty.
55
+ # Returns `String` to match the 4.x public contract (`Git::Lib#command`
56
+ # returned `result.stdout`).
57
+ #
58
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
59
+ #
60
+ def gc
61
+ Git::Commands::Gc.new(@execution_context).call(prune: true, aggressive: true, auto: true).stdout
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,451 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+ require 'git/commands/diff'
5
+ require 'git/commands/merge/start'
6
+ require 'git/commands/merge_base'
7
+ require 'git/commands/revert/start'
8
+ require 'git/commands/show'
9
+ require 'git/repository/shared_private'
10
+
11
+ module Git
12
+ class Repository
13
+ # Facade methods for merge operations: merging branches into the current branch
14
+ # or into another branch, and finding common ancestors between commits
15
+ #
16
+ # Included by {Git::Repository}.
17
+ #
18
+ # @api private
19
+ #
20
+ module Merging
21
+ # Option keys accepted by {#merge}
22
+ #
23
+ # Derived from the 4.x option map for `Git::Lib#merge`.
24
+ MERGE_ALLOWED_OPTS = %i[no_commit no_ff m message].freeze
25
+ private_constant :MERGE_ALLOWED_OPTS
26
+
27
+ # Option keys accepted by {#merge_base}
28
+ #
29
+ # Derived from the 4.x option map for `Git::Lib#merge_base`.
30
+ MERGE_BASE_ALLOWED_OPTS = %i[octopus independent fork_point all].freeze
31
+ private_constant :MERGE_BASE_ALLOWED_OPTS
32
+
33
+ # Merge one or more branches into the current branch
34
+ #
35
+ # The merge commit message may be given by the message positional argument, the
36
+ # `:message` option, or the `:m` option; if more than one is provided, the
37
+ # precedence is positional argument > `:message` > `:m`.
38
+ #
39
+ # @example Merge a single branch
40
+ # repo.merge('feature')
41
+ #
42
+ # @example Merge a branch with a no-fast-forward commit message
43
+ # repo.merge('feature', 'Merge feature into main', no_ff: true)
44
+ #
45
+ # @example Octopus merge of multiple branches
46
+ # repo.merge(%w[feature-a feature-b])
47
+ #
48
+ # @example Merge without committing
49
+ # repo.merge('feature', nil, no_commit: true)
50
+ #
51
+ # @param branch [#to_s, Array<#to_s>] the branch or branches to merge into the
52
+ # current branch
53
+ #
54
+ # When an Array is given, an octopus merge is performed; each branch-ish
55
+ # object (e.g., {Git::BranchInfo}) is coerced to a String via `#to_s`.
56
+ #
57
+ # @param message [String, nil] optional commit message for the merge commit
58
+ #
59
+ # Translated to the `-m` flag internally. For fast-forward merges git ignores
60
+ # this value; use `no_ff: true` to ensure a merge commit is created and the
61
+ # message is recorded.
62
+ #
63
+ # @param opts [Hash] additional options forwarded to `git merge`
64
+ #
65
+ # @option opts [Boolean, nil] :no_commit (nil) stop before creating the merge commit
66
+ # (`--no-commit`)
67
+ #
68
+ # @option opts [Boolean, nil] :no_ff (nil) create a merge commit even when
69
+ # fast-forward is possible (`--no-ff`)
70
+ #
71
+ # @option opts [String] :message (nil) commit message
72
+ #
73
+ # Prefer the `:m` option instead of this one. Translated to the `-m` flag.
74
+ # Identical to the positional `message` argument and the `:m` option.
75
+ #
76
+ # @option opts [String] :m (nil) commit message (`-m` flag)
77
+ #
78
+ # @return [String] git's stdout from the merge command
79
+ #
80
+ # @raise [ArgumentError] when unsupported options are provided
81
+ #
82
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
83
+ #
84
+ def merge(branch, message = nil, opts = {})
85
+ SharedPrivate.assert_valid_opts!(MERGE_ALLOWED_OPTS, **opts)
86
+
87
+ # Dup so callers who reuse the same opts hash are not affected
88
+ opts = opts.dup
89
+
90
+ # Merge positional message into opts so the rest of the logic is uniform
91
+ opts[:message] = message if message
92
+
93
+ # git merge uses -m, not --message; translate the key
94
+ opts[:m] = opts.delete(:message) if opts.key?(:message)
95
+
96
+ branches = Array(branch).map(&:to_s)
97
+ Git::Commands::Merge::Start.new(@execution_context).call(*branches, no_edit: true, **opts).stdout
98
+ end
99
+
100
+ # Option keys accepted by {#merge_into}
101
+ #
102
+ # The keys accepted by {#merge} except `:no_commit`. A merge stopped before
103
+ # its commit leaves `target_branch` unchanged, and the restore checkout would
104
+ # carry the staged merge result onto the original branch instead.
105
+ MERGE_INTO_ALLOWED_OPTS = %i[no_ff m message].freeze
106
+ private_constant :MERGE_INTO_ALLOWED_OPTS
107
+
108
+ # Merge one or more branches into another branch without leaving the current branch
109
+ #
110
+ # Records the current branch (or the current commit when HEAD is detached),
111
+ # checks out `target_branch`, merges `branch` into it with {#merge}, then
112
+ # checks out the original branch or commit again. Use {#merge} directly when
113
+ # the target is the currently checked-out branch.
114
+ #
115
+ # `target_branch` must be an existing local branch. Unlike {#checkout}, a
116
+ # commit SHA, tag, or remote-tracking branch is rejected before any checkout
117
+ # happens: those detach HEAD, and the merge commit made there would be left
118
+ # dangling once the original branch is restored while the named ref stayed
119
+ # unchanged.
120
+ #
121
+ # HEAD must be on a branch with at least one commit, or detached: an unborn
122
+ # branch (no commits yet) cannot be checked out again by name, so it is
123
+ # rejected before any checkout happens.
124
+ #
125
+ # Option keys, the source list, and `target_branch` are checked before any
126
+ # branch is checked out, so those failures never leave the repository on
127
+ # `target_branch`. Anything rejected later, such as an option value that is
128
+ # not accepted or a source ref that does not exist, surfaces inside {#merge}
129
+ # after the checkout and follows the Note below.
130
+ #
131
+ # The `:no_commit` option is not accepted: a merge stopped before its commit
132
+ # would leave `target_branch` unchanged and the restore checkout would carry
133
+ # the staged result onto the original branch. To merge without committing,
134
+ # call {#checkout} and {#merge} directly.
135
+ #
136
+ # **Note:** the restore checkout is not wrapped in `ensure`. If the merge
137
+ # fails (for example, on a conflict), the repository is left checked out on
138
+ # `target_branch` with the merge in progress rather than restored to the
139
+ # original branch.
140
+ #
141
+ # @example Merge a feature branch into main while staying on the current branch
142
+ # repo.merge_into('main', 'feature')
143
+ #
144
+ # @example Merge with a no-fast-forward commit message
145
+ # repo.merge_into('main', 'feature', 'Merge feature into main', no_ff: true)
146
+ #
147
+ # @example Octopus merge of multiple branches into main
148
+ # repo.merge_into('main', %w[feature-a feature-b])
149
+ #
150
+ # @param target_branch [String] the name of an existing local branch to
151
+ # merge into
152
+ #
153
+ # @param branch [#to_s, Array<#to_s>] the branch or branches to merge into
154
+ # `target_branch`; accepts the same forms as {#merge}, but must name at
155
+ # least one branch
156
+ #
157
+ # @param message [String, nil] optional commit message for the merge commit;
158
+ # see {#merge} for how it interacts with the `:message` and `:m` options
159
+ #
160
+ # @param opts [Hash] additional options forwarded to {#merge}
161
+ #
162
+ # @option opts [Boolean, nil] :no_ff (nil) create a merge commit even when
163
+ # fast-forward is possible (`--no-ff`)
164
+ #
165
+ # @option opts [String] :message (nil) commit message; prefer the `:m` option
166
+ #
167
+ # @option opts [String] :m (nil) commit message (`-m` flag)
168
+ #
169
+ # @return [String] git's stdout from the merge command
170
+ #
171
+ # @raise [ArgumentError] when unsupported options (including `:no_commit`)
172
+ # are provided
173
+ #
174
+ # @raise [ArgumentError] when `branch` is `nil` or an empty Array
175
+ #
176
+ # @raise [ArgumentError] when `target_branch` is not an existing local branch
177
+ #
178
+ # @raise [Git::Error] when HEAD is on an unborn branch
179
+ #
180
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
181
+ #
182
+ def merge_into(target_branch, branch, message = nil, opts = {})
183
+ SharedPrivate.assert_valid_opts!(MERGE_INTO_ALLOWED_OPTS, **opts)
184
+ raise ArgumentError, 'at least one branch to merge is required' if Array(branch).empty?
185
+
186
+ SharedPrivate.assert_local_branch!(self, target_branch)
187
+ restore_point = SharedPrivate.head_restore_point(self)
188
+ checkout(target_branch)
189
+ output = merge(branch, message, opts)
190
+ checkout(restore_point)
191
+ output
192
+ end
193
+
194
+ # Find common ancestor commit(s) for use in a merge
195
+ #
196
+ # @example Find the common ancestor of two branches
197
+ # repo.merge_base('main', 'feature') #=> ["abc123def456..."]
198
+ #
199
+ # @example Find all common ancestors of two branches
200
+ # repo.merge_base('branch-a', 'branch-b', all: true)
201
+ #
202
+ # @example Find the fork point of a branch (consults the reflog)
203
+ # repo.merge_base('main', 'feature', fork_point: true)
204
+ #
205
+ # @example Find independent commits not reachable from each other
206
+ # repo.merge_base('abc1234', 'main', 'feature', independent: true)
207
+ #
208
+ # @overload merge_base(*commits, options = {})
209
+ #
210
+ # @param commits [Array<String>] two or more commit SHAs, branch names,
211
+ # or refs to find the common ancestor(s) of
212
+ #
213
+ # @param options [Hash] merge-base options
214
+ #
215
+ # @option options [Boolean, nil] :octopus (nil) compute the best common
216
+ # ancestor for an n-way merge (intersection of all merge bases)
217
+ #
218
+ # @option options [Boolean, nil] :independent (nil) list commits not
219
+ # reachable from any other; useful for finding minimal merge points
220
+ #
221
+ # @option options [Boolean, nil] :fork_point (nil) find the fork point
222
+ # where a branch diverged from another, consulting the reflog
223
+ #
224
+ # @option options [Boolean, nil] :all (nil) output all merge bases instead
225
+ # of just the first when multiple equally good bases exist
226
+ #
227
+ # @return [Array<String>] commit SHAs of the common ancestor(s); empty
228
+ # when no common ancestor exists or `--fork-point` finds none
229
+ #
230
+ # @raise [ArgumentError] when unsupported options are provided
231
+ #
232
+ # @raise [Git::FailedError] when `git merge-base` exits outside the
233
+ # allowed range (exit code > 1)
234
+ #
235
+ def merge_base(*args)
236
+ opts = args.last.is_a?(Hash) ? args.pop : {}
237
+ SharedPrivate.assert_valid_opts!(MERGE_BASE_ALLOWED_OPTS, **opts)
238
+ result = Git::Commands::MergeBase.new(@execution_context).call(*args, **opts)
239
+ result.stdout.lines.map(&:strip).reject(&:empty?)
240
+ end
241
+
242
+ # Return the paths of files with unresolved merge conflicts
243
+ #
244
+ # @example List conflicting files after a failed merge
245
+ # paths = repo.unmerged
246
+ # # => ["config/settings.rb", "lib/git/base.rb"]
247
+ # paths.each { |path| puts "Conflict in #{path}" }
248
+ #
249
+ # @return [Array<String>] repository-relative paths of files with unresolved
250
+ # merge conflicts; empty array when the working tree has no conflicts
251
+ #
252
+ # @raise [Git::FailedError] if git exits outside the allowed range (exit code > 2)
253
+ #
254
+ # @see #each_conflict
255
+ #
256
+ def unmerged
257
+ Private.unmerged_paths(@execution_context)
258
+ end
259
+
260
+ # Iterate over files with merge conflicts, yielding conflict details for each
261
+ #
262
+ # For each unmerged file, the staged content for both sides of the conflict
263
+ # (stage 2 "ours" and stage 3 "theirs") is written to temporary files whose
264
+ # paths are yielded alongside the file path. The temporary files are deleted
265
+ # automatically when the block returns.
266
+ #
267
+ # @example Inspect conflicting files
268
+ # repo.each_conflict do |file, your_version, their_version|
269
+ # puts "Conflict in #{file}"
270
+ # puts "Your version:"
271
+ # puts File.read(your_version)
272
+ # puts "Their version:"
273
+ # puts File.read(their_version)
274
+ # end
275
+ #
276
+ # @return [Array<String>] the list of unmerged file paths
277
+ #
278
+ # @raise [Git::FailedError] when `git diff --cached` exits outside the
279
+ # allowed range (exit code > 2)
280
+ #
281
+ # @yield [file, your_version, their_version] passes conflict details for
282
+ # each unmerged file
283
+ #
284
+ # @yieldparam file [String] path to the conflicting file, relative to the
285
+ # working tree
286
+ #
287
+ # @yieldparam your_version [String] path to a temporary file containing the
288
+ # stage-2 (ours) content for the conflicting file
289
+ #
290
+ # @yieldparam their_version [String] path to a temporary file containing the
291
+ # stage-3 (theirs) content for the conflicting file
292
+ #
293
+ # @yieldreturn [void]
294
+ #
295
+ def each_conflict
296
+ Private.unmerged_paths(@execution_context).each do |file_path|
297
+ Private.write_staged_file(@execution_context, file_path, 2) do |your_file|
298
+ Private.write_staged_file(@execution_context, file_path, 3) do |their_file|
299
+ yield(file_path, your_file.path, their_file.path)
300
+ end
301
+ end
302
+ end
303
+ end
304
+
305
+ # Iterate over files with merge conflicts, yielding conflict details for each
306
+ #
307
+ # For each unmerged file, the staged content for both sides of the conflict
308
+ # (stage 2 "ours" and stage 3 "theirs") is written to temporary files whose
309
+ # paths are yielded alongside the file path. The temporary files are deleted
310
+ # automatically when the block returns.
311
+ #
312
+ # @example Inspect conflicting files
313
+ # repo.conflicts do |file, your_version, their_version|
314
+ # puts "Conflict in #{file}"
315
+ # puts File.read(your_version)
316
+ # puts File.read(their_version)
317
+ # end
318
+ #
319
+ # @return [Array<String>] the list of unmerged file paths
320
+ #
321
+ # @raise [Git::FailedError] when `git diff --cached` exits outside the
322
+ # allowed range (exit code > 2)
323
+ #
324
+ # @yield [file, your_version, their_version] passes conflict details for
325
+ # each unmerged file
326
+ #
327
+ # @yieldparam file [String] path to the conflicting file, relative to the
328
+ # working tree
329
+ #
330
+ # @yieldparam your_version [String] path to a temporary file containing the
331
+ # stage-2 (ours) content for the conflicting file
332
+ #
333
+ # @yieldparam their_version [String] path to a temporary file containing the
334
+ # stage-3 (theirs) content for the conflicting file
335
+ #
336
+ # @yieldreturn [void]
337
+ #
338
+ # @deprecated Use {#each_conflict} instead
339
+ #
340
+ def conflicts(&)
341
+ Git::Deprecation.warn(
342
+ 'Git::Repository#conflicts is deprecated and will be removed in v6.0.0. ' \
343
+ 'Use Git::Repository#each_conflict instead.'
344
+ )
345
+ each_conflict(&)
346
+ end
347
+
348
+ # Option keys accepted by {#revert}
349
+ #
350
+ # Derived from the 4.x option map for `Git::Lib#revert`.
351
+ REVERT_ALLOWED_OPTS = %i[no_edit].freeze
352
+ private_constant :REVERT_ALLOWED_OPTS
353
+
354
+ # Revert one or more existing commits by creating new commits that undo
355
+ # the changes those commits introduced
356
+ #
357
+ # The working tree must be clean before calling this method. By default
358
+ # the editor is suppressed (`--no-edit`) so the commit message is taken
359
+ # from git's default revert message without prompting.
360
+ #
361
+ # @example Revert the most recent commit
362
+ # repo.revert('HEAD')
363
+ #
364
+ # @example Revert a specific commit by SHA
365
+ # repo.revert('abc1234')
366
+ #
367
+ # @example Revert a range of commits
368
+ # repo.revert('HEAD~3..HEAD~1')
369
+ #
370
+ # @example Revert without suppressing the editor
371
+ # repo.revert('HEAD', no_edit: false)
372
+ #
373
+ # @param commitish [String, nil] the commit, ref, or rev range to revert;
374
+ # see `gitrevisions(7)` for accepted forms; defaults to `'HEAD'` when
375
+ # `nil`
376
+ #
377
+ # @param opts [Hash] additional options forwarded to `git revert`
378
+ #
379
+ # @option opts [Boolean, nil] :no_edit (true) suppress the commit-message
380
+ # editor (`--no-edit`); pass `false` to open the editor
381
+ #
382
+ # @return [String] git's stdout from the revert command
383
+ #
384
+ # @raise [ArgumentError] when unsupported options are provided
385
+ #
386
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
387
+ #
388
+ def revert(commitish = nil, opts = {})
389
+ commitish = 'HEAD' if commitish.nil?
390
+ SharedPrivate.assert_valid_opts!(REVERT_ALLOWED_OPTS, **opts)
391
+ opts = { no_edit: true }.merge(opts)
392
+ Git::Commands::Revert::Start.new(@execution_context).call(commitish, **opts).stdout
393
+ end
394
+
395
+ # Private helpers local to {Git::Repository::Merging}
396
+ #
397
+ # @api private
398
+ module Private
399
+ # Tempfile name prefixes for staged content, keyed by git stage index
400
+ STAGE_PREFIXES = { 2 => 'YOUR-', 3 => 'THEIR-' }.freeze
401
+
402
+ module_function
403
+
404
+ # Returns the list of file paths with unresolved merge conflicts
405
+ #
406
+ # @param execution_context [Git::ExecutionContext] the execution context
407
+ # used to run git commands
408
+ #
409
+ # @return [Array<String>] unmerged file paths
410
+ #
411
+ # @api private
412
+ #
413
+ def unmerged_paths(execution_context)
414
+ result = Git::Commands::Diff.new(execution_context).call(cached: true)
415
+ result.stdout.split("\n").filter_map do |line|
416
+ ::Regexp.last_match(1) if line =~ /^\* Unmerged path (.*)/
417
+ end
418
+ end
419
+
420
+ # Creates a Tempfile with the staged content for `file_path` at `stage`
421
+ # and yields the open IO object to the block
422
+ #
423
+ # @param execution_context [Git::ExecutionContext] the execution context
424
+ # used to run git commands
425
+ #
426
+ # @param file_path [String] repository-relative path to the conflicting file
427
+ #
428
+ # @param stage [Integer] git stage index (2 = ours, 3 = theirs)
429
+ #
430
+ # @return [void]
431
+ #
432
+ # @yield [f] yields the open Tempfile containing the staged content
433
+ #
434
+ # @yieldparam f [Tempfile] open IO object for the staged content
435
+ #
436
+ # @yieldreturn [void]
437
+ #
438
+ # @api private
439
+ #
440
+ def write_staged_file(execution_context, file_path, stage)
441
+ Tempfile.create([STAGE_PREFIXES[stage], File.basename(file_path)]) do |f|
442
+ Git::Commands::Show.new(execution_context).call(":#{stage}:#{file_path}", out: f)
443
+ f.flush
444
+ yield f
445
+ end
446
+ end
447
+ end
448
+ private_constant :Private
449
+ end
450
+ end
451
+ end