git 4.3.2 → 5.0.0.beta.1

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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +67 -2705
  3. data/.github/pull_request_template.md +3 -1
  4. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  5. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  6. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  7. data/.github/skills/command-implementation/SKILL.md +229 -0
  8. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  9. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  10. data/.github/skills/dependency-management/SKILL.md +72 -0
  11. data/.github/skills/development-workflow/SKILL.md +506 -0
  12. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  13. data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
  14. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  15. data/.github/skills/facade-implementation/SKILL.md +260 -0
  16. data/.github/skills/facade-test-conventions/SKILL.md +380 -0
  17. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  18. data/.github/skills/make-skill-template/SKILL.md +176 -0
  19. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  20. data/.github/skills/project-context/SKILL.md +313 -0
  21. data/.github/skills/pull-request-review/SKILL.md +168 -0
  22. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  23. data/.github/skills/release-management/SKILL.md +125 -0
  24. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  25. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  26. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  27. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  28. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  29. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  30. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  31. data/.github/skills/test-debugging/SKILL.md +160 -0
  32. data/.github/skills/yard-documentation/SKILL.md +793 -0
  33. data/.github/workflows/continuous_integration.yml +3 -2
  34. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  35. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  36. data/.github/workflows/release.yml +3 -4
  37. data/.gitignore +8 -0
  38. data/.husky/pre-commit +13 -0
  39. data/.release-please-manifest.json +1 -1
  40. data/.rspec +3 -0
  41. data/.rubocop.yml +7 -3
  42. data/.rubocop_todo.yml +23 -5
  43. data/.yardopts +1 -0
  44. data/CHANGELOG.md +0 -40
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -5
  47. data/Rakefile +61 -9
  48. data/commitlint.test +4 -0
  49. data/git.gemspec +14 -8
  50. data/lib/git/args_builder.rb +0 -8
  51. data/lib/git/base.rb +486 -410
  52. data/lib/git/branch.rb +380 -43
  53. data/lib/git/branch_delete_failure.rb +31 -0
  54. data/lib/git/branch_delete_result.rb +63 -0
  55. data/lib/git/branch_info.rb +178 -0
  56. data/lib/git/branches.rb +130 -24
  57. data/lib/git/command_line/base.rb +245 -0
  58. data/lib/git/command_line/capturing.rb +249 -0
  59. data/lib/git/command_line/result.rb +96 -0
  60. data/lib/git/command_line/streaming.rb +194 -0
  61. data/lib/git/command_line.rb +43 -322
  62. data/lib/git/command_line_result.rb +4 -88
  63. data/lib/git/commands/add.rb +131 -0
  64. data/lib/git/commands/am/abort.rb +43 -0
  65. data/lib/git/commands/am/apply.rb +252 -0
  66. data/lib/git/commands/am/continue.rb +43 -0
  67. data/lib/git/commands/am/quit.rb +43 -0
  68. data/lib/git/commands/am/retry.rb +47 -0
  69. data/lib/git/commands/am/show_current_patch.rb +64 -0
  70. data/lib/git/commands/am/skip.rb +42 -0
  71. data/lib/git/commands/am.rb +33 -0
  72. data/lib/git/commands/apply.rb +237 -0
  73. data/lib/git/commands/archive/list_formats.rb +46 -0
  74. data/lib/git/commands/archive.rb +140 -0
  75. data/lib/git/commands/arguments.rb +3510 -0
  76. data/lib/git/commands/base.rb +403 -0
  77. data/lib/git/commands/branch/copy.rb +94 -0
  78. data/lib/git/commands/branch/create.rb +173 -0
  79. data/lib/git/commands/branch/delete.rb +80 -0
  80. data/lib/git/commands/branch/list.rb +162 -0
  81. data/lib/git/commands/branch/move.rb +94 -0
  82. data/lib/git/commands/branch/set_upstream.rb +86 -0
  83. data/lib/git/commands/branch/show_current.rb +49 -0
  84. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  85. data/lib/git/commands/branch.rb +34 -0
  86. data/lib/git/commands/cat_file/batch.rb +364 -0
  87. data/lib/git/commands/cat_file/filtered.rb +105 -0
  88. data/lib/git/commands/cat_file/raw.rb +210 -0
  89. data/lib/git/commands/cat_file.rb +49 -0
  90. data/lib/git/commands/checkout/branch.rb +151 -0
  91. data/lib/git/commands/checkout/files.rb +115 -0
  92. data/lib/git/commands/checkout.rb +38 -0
  93. data/lib/git/commands/checkout_index.rb +105 -0
  94. data/lib/git/commands/clean.rb +100 -0
  95. data/lib/git/commands/clone.rb +240 -0
  96. data/lib/git/commands/commit.rb +272 -0
  97. data/lib/git/commands/commit_tree.rb +100 -0
  98. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  99. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  100. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  101. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  102. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  103. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  104. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
  105. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  106. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  107. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  108. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  109. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  110. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  111. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  112. data/lib/git/commands/config_option_syntax.rb +56 -0
  113. data/lib/git/commands/describe.rb +155 -0
  114. data/lib/git/commands/diff.rb +656 -0
  115. data/lib/git/commands/diff_files.rb +518 -0
  116. data/lib/git/commands/diff_index.rb +496 -0
  117. data/lib/git/commands/fetch.rb +352 -0
  118. data/lib/git/commands/fsck.rb +136 -0
  119. data/lib/git/commands/gc.rb +132 -0
  120. data/lib/git/commands/grep.rb +338 -0
  121. data/lib/git/commands/init.rb +99 -0
  122. data/lib/git/commands/log.rb +632 -0
  123. data/lib/git/commands/ls_files.rb +191 -0
  124. data/lib/git/commands/ls_remote.rb +155 -0
  125. data/lib/git/commands/ls_tree.rb +131 -0
  126. data/lib/git/commands/maintenance/register.rb +75 -0
  127. data/lib/git/commands/maintenance/run.rb +104 -0
  128. data/lib/git/commands/maintenance/start.rb +66 -0
  129. data/lib/git/commands/maintenance/stop.rb +55 -0
  130. data/lib/git/commands/maintenance/unregister.rb +79 -0
  131. data/lib/git/commands/maintenance.rb +31 -0
  132. data/lib/git/commands/merge/abort.rb +44 -0
  133. data/lib/git/commands/merge/continue.rb +44 -0
  134. data/lib/git/commands/merge/quit.rb +46 -0
  135. data/lib/git/commands/merge/start.rb +245 -0
  136. data/lib/git/commands/merge.rb +28 -0
  137. data/lib/git/commands/merge_base.rb +86 -0
  138. data/lib/git/commands/mv.rb +77 -0
  139. data/lib/git/commands/name_rev.rb +114 -0
  140. data/lib/git/commands/pull.rb +377 -0
  141. data/lib/git/commands/push.rb +246 -0
  142. data/lib/git/commands/read_tree.rb +149 -0
  143. data/lib/git/commands/remote/add.rb +91 -0
  144. data/lib/git/commands/remote/get_url.rb +66 -0
  145. data/lib/git/commands/remote/list.rb +54 -0
  146. data/lib/git/commands/remote/prune.rb +61 -0
  147. data/lib/git/commands/remote/remove.rb +52 -0
  148. data/lib/git/commands/remote/rename.rb +69 -0
  149. data/lib/git/commands/remote/set_branches.rb +63 -0
  150. data/lib/git/commands/remote/set_head.rb +82 -0
  151. data/lib/git/commands/remote/set_url.rb +71 -0
  152. data/lib/git/commands/remote/set_url_add.rb +61 -0
  153. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  154. data/lib/git/commands/remote/show.rb +71 -0
  155. data/lib/git/commands/remote/update.rb +72 -0
  156. data/lib/git/commands/remote.rb +42 -0
  157. data/lib/git/commands/repack.rb +277 -0
  158. data/lib/git/commands/reset.rb +147 -0
  159. data/lib/git/commands/rev_parse.rb +297 -0
  160. data/lib/git/commands/revert/abort.rb +45 -0
  161. data/lib/git/commands/revert/continue.rb +57 -0
  162. data/lib/git/commands/revert/quit.rb +47 -0
  163. data/lib/git/commands/revert/skip.rb +44 -0
  164. data/lib/git/commands/revert/start.rb +153 -0
  165. data/lib/git/commands/revert.rb +29 -0
  166. data/lib/git/commands/rm.rb +114 -0
  167. data/lib/git/commands/show.rb +632 -0
  168. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  169. data/lib/git/commands/show_ref/exists.rb +78 -0
  170. data/lib/git/commands/show_ref/list.rb +145 -0
  171. data/lib/git/commands/show_ref/verify.rb +120 -0
  172. data/lib/git/commands/show_ref.rb +42 -0
  173. data/lib/git/commands/stash/apply.rb +75 -0
  174. data/lib/git/commands/stash/branch.rb +65 -0
  175. data/lib/git/commands/stash/clear.rb +41 -0
  176. data/lib/git/commands/stash/create.rb +58 -0
  177. data/lib/git/commands/stash/drop.rb +67 -0
  178. data/lib/git/commands/stash/list.rb +39 -0
  179. data/lib/git/commands/stash/pop.rb +78 -0
  180. data/lib/git/commands/stash/push.rb +103 -0
  181. data/lib/git/commands/stash/show.rb +149 -0
  182. data/lib/git/commands/stash/store.rb +63 -0
  183. data/lib/git/commands/stash.rb +38 -0
  184. data/lib/git/commands/status.rb +169 -0
  185. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  186. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  187. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  188. data/lib/git/commands/symbolic_ref.rb +38 -0
  189. data/lib/git/commands/tag/create.rb +139 -0
  190. data/lib/git/commands/tag/delete.rb +55 -0
  191. data/lib/git/commands/tag/list.rb +143 -0
  192. data/lib/git/commands/tag/verify.rb +71 -0
  193. data/lib/git/commands/tag.rb +26 -0
  194. data/lib/git/commands/update_ref/batch.rb +140 -0
  195. data/lib/git/commands/update_ref/delete.rb +92 -0
  196. data/lib/git/commands/update_ref/update.rb +106 -0
  197. data/lib/git/commands/update_ref.rb +42 -0
  198. data/lib/git/commands/version.rb +52 -0
  199. data/lib/git/commands/worktree/add.rb +140 -0
  200. data/lib/git/commands/worktree/list.rb +64 -0
  201. data/lib/git/commands/worktree/lock.rb +58 -0
  202. data/lib/git/commands/worktree/management_base.rb +51 -0
  203. data/lib/git/commands/worktree/move.rb +66 -0
  204. data/lib/git/commands/worktree/prune.rb +67 -0
  205. data/lib/git/commands/worktree/remove.rb +63 -0
  206. data/lib/git/commands/worktree/repair.rb +76 -0
  207. data/lib/git/commands/worktree/unlock.rb +47 -0
  208. data/lib/git/commands/worktree.rb +43 -0
  209. data/lib/git/commands/write_tree.rb +68 -0
  210. data/lib/git/commands.rb +89 -0
  211. data/lib/git/detached_head_info.rb +54 -0
  212. data/lib/git/diff.rb +297 -7
  213. data/lib/git/diff_file_numstat_info.rb +29 -0
  214. data/lib/git/diff_file_patch_info.rb +134 -0
  215. data/lib/git/diff_file_raw_info.rb +127 -0
  216. data/lib/git/diff_info.rb +169 -0
  217. data/lib/git/diff_path_status.rb +78 -19
  218. data/lib/git/diff_result.rb +32 -0
  219. data/lib/git/diff_stats.rb +59 -14
  220. data/lib/git/dirstat_info.rb +86 -0
  221. data/lib/git/errors.rb +65 -2
  222. data/lib/git/execution_context/global.rb +56 -0
  223. data/lib/git/execution_context/repository.rb +147 -0
  224. data/lib/git/execution_context.rb +482 -0
  225. data/lib/git/file_ref.rb +74 -0
  226. data/lib/git/fsck_object.rb +9 -9
  227. data/lib/git/fsck_result.rb +1 -1
  228. data/lib/git/lib.rb +1606 -1028
  229. data/lib/git/log.rb +15 -2
  230. data/lib/git/object.rb +92 -22
  231. data/lib/git/parsers/branch.rb +224 -0
  232. data/lib/git/parsers/cat_file.rb +111 -0
  233. data/lib/git/parsers/diff.rb +585 -0
  234. data/lib/git/parsers/fsck.rb +133 -0
  235. data/lib/git/parsers/grep.rb +42 -0
  236. data/lib/git/parsers/ls_tree.rb +58 -0
  237. data/lib/git/parsers/stash.rb +208 -0
  238. data/lib/git/parsers/tag.rb +257 -0
  239. data/lib/git/remote.rb +133 -9
  240. data/lib/git/repository/branching.rb +572 -0
  241. data/lib/git/repository/committing.rb +191 -0
  242. data/lib/git/repository/configuring.rb +156 -0
  243. data/lib/git/repository/diffing.rb +775 -0
  244. data/lib/git/repository/inspecting.rb +153 -0
  245. data/lib/git/repository/logging.rb +247 -0
  246. data/lib/git/repository/merging.rb +295 -0
  247. data/lib/git/repository/object_operations.rb +1101 -0
  248. data/lib/git/repository/path_resolver.rb +207 -0
  249. data/lib/git/repository/remote_operations.rb +753 -0
  250. data/lib/git/repository/shared_private.rb +51 -0
  251. data/lib/git/repository/staging.rb +390 -0
  252. data/lib/git/repository/stashing.rb +107 -0
  253. data/lib/git/repository/status_operations.rb +180 -0
  254. data/lib/git/repository/worktree_operations.rb +159 -0
  255. data/lib/git/repository.rb +264 -1
  256. data/lib/git/stash.rb +85 -4
  257. data/lib/git/stash_info.rb +104 -0
  258. data/lib/git/stashes.rb +130 -13
  259. data/lib/git/status.rb +224 -18
  260. data/lib/git/tag_delete_failure.rb +31 -0
  261. data/lib/git/tag_delete_result.rb +63 -0
  262. data/lib/git/tag_info.rb +105 -0
  263. data/lib/git/version.rb +109 -2
  264. data/lib/git/version_constraint.rb +81 -0
  265. data/lib/git/worktree.rb +120 -5
  266. data/lib/git/worktrees.rb +107 -7
  267. data/lib/git.rb +114 -18
  268. data/redesign/1_architecture_existing.md +54 -18
  269. data/redesign/2_architecture_redesign.md +365 -46
  270. data/redesign/3_architecture_implementation.md +1451 -54
  271. data/tasks/gem_tasks.rake +4 -0
  272. data/tasks/npm_tasks.rake +7 -0
  273. data/tasks/rspec.rake +48 -0
  274. data/tasks/test.rake +13 -1
  275. data/tasks/yard.rake +34 -7
  276. metadata +349 -20
  277. data/lib/git/index.rb +0 -6
  278. data/lib/git/path.rb +0 -38
  279. data/lib/git/working_directory.rb +0 -6
  280. /data/{release-please-config.json → .release-please-config.json} +0 -0
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/fsck'
4
+ require 'git/commands/show'
5
+ require 'git/parsers/fsck'
6
+ require 'git/repository/shared_private'
7
+
8
+ module Git
9
+ class Repository
10
+ # Facade methods for read-only repository inspection operations
11
+ #
12
+ # These methods report on the contents and integrity of the repository.
13
+ #
14
+ # Included by {Git::Repository}.
15
+ #
16
+ # @api public
17
+ #
18
+ module Inspecting
19
+ # Show a single git object (a commit, tag, tree, or blob)
20
+ #
21
+ # @example Show the HEAD commit
22
+ # repo.show
23
+ #
24
+ # @example Show a specific commit
25
+ # repo.show('HEAD~1')
26
+ #
27
+ # @example Show the contents of a file at a revision
28
+ # repo.show('HEAD', 'README.md')
29
+ #
30
+ # @param objectish [String, nil] the object to show; a ref, SHA, or
31
+ # `objectish:path` expression
32
+ #
33
+ # Defaults to `HEAD` when `nil`.
34
+ #
35
+ # @param path [String, nil] the file whose contents to show at `objectish`,
36
+ # when given
37
+ #
38
+ # Combined with `objectish` as `objectish:path`. When `objectish` is `nil`
39
+ # and `path` is given, `HEAD` is used as the objectish, so
40
+ # `show(nil, 'README.md')` resolves to `HEAD:README.md`.
41
+ #
42
+ # @return [String] git's stdout from the show, with trailing newlines
43
+ # preserved
44
+ #
45
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
46
+ #
47
+ def show(objectish = nil, path = nil)
48
+ object = path ? "#{objectish || 'HEAD'}:#{path}" : objectish
49
+ Git::Commands::Show.new(@execution_context).call(*[object].compact).stdout
50
+ end
51
+
52
+ # Option keys accepted by {#fsck}
53
+ #
54
+ # `:progress`/`:no_progress` are intentionally excluded: progress output is
55
+ # always suppressed (see {#fsck}), so callers may not toggle it.
56
+ FSCK_ALLOWED_OPTS = %i[
57
+ tags root unreachable cache no_reflogs
58
+ full no_full strict verbose lost_found dangling no_dangling
59
+ connectivity_only name_objects no_name_objects references no_references
60
+ ].freeze
61
+ private_constant :FSCK_ALLOWED_OPTS
62
+
63
+ # Verify the connectivity and validity of the objects in the database
64
+ #
65
+ # Runs `git fsck` and returns the categorized objects it flags. Progress
66
+ # output is always suppressed (`--no-progress`) so that stdout contains only
67
+ # the machine-parsable findings.
68
+ #
69
+ # @overload fsck(*objects, **options)
70
+ #
71
+ # @example Check repository integrity
72
+ # result = repo.fsck
73
+ # result.dangling.each { |obj| puts "#{obj.type}: #{obj.oid}" }
74
+ #
75
+ # @example Check if the repository is clean
76
+ # repo.fsck.empty? #=> true
77
+ #
78
+ # @example List root commits
79
+ # repo.fsck(root: true).root.each { |obj| puts obj.oid }
80
+ #
81
+ # @example Check specific objects
82
+ # repo.fsck('abc1234', 'def5678')
83
+ #
84
+ # @param objects [Array<String>] specific objects to treat as heads for the
85
+ # unreachability trace
86
+ #
87
+ # When none are given, git fsck defaults to the index file, all refs, and
88
+ # all reflogs.
89
+ #
90
+ # @param options [Hash] options for the fsck command
91
+ #
92
+ # @option options [Boolean, nil] :tags (nil) report tags
93
+ #
94
+ # @option options [Boolean, nil] :root (nil) report root nodes
95
+ #
96
+ # @option options [Boolean, nil] :unreachable (nil) print objects that exist
97
+ # but are not reachable from any reference node
98
+ #
99
+ # @option options [Boolean, nil] :cache (nil) consider objects recorded in the
100
+ # index as head nodes for reachability
101
+ #
102
+ # @option options [Boolean, nil] :no_reflogs (nil) do not consider commits
103
+ # referenced only by reflogs to be reachable
104
+ #
105
+ # @option options [Boolean, nil] :full (nil) also check alternate object
106
+ # pools and packed archives, not just the local store
107
+ #
108
+ # @option options [Boolean, nil] :no_full (nil) skip alternate object pools and
109
+ # packed archives
110
+ #
111
+ # @option options [Boolean, nil] :strict (nil) enable stricter checking
112
+ #
113
+ # @option options [Boolean, nil] :verbose (nil) be chatty
114
+ #
115
+ # @option options [Boolean, nil] :lost_found (nil) write dangling objects
116
+ # into `.git/lost-found`
117
+ #
118
+ # This modifies the repository by creating files.
119
+ #
120
+ # @option options [Boolean, nil] :dangling (nil) print dangling objects
121
+ #
122
+ # @option options [Boolean, nil] :no_dangling (nil) suppress dangling object
123
+ # reporting
124
+ #
125
+ # @option options [Boolean, nil] :connectivity_only (nil) check only
126
+ # connectivity; faster but does not validate blob content
127
+ #
128
+ # @option options [Boolean, nil] :name_objects (nil) show the name of each
129
+ # reachable object alongside its identifier
130
+ #
131
+ # @option options [Boolean, nil] :no_name_objects (nil) suppress object name
132
+ # display
133
+ #
134
+ # @option options [Boolean, nil] :references (nil) check reference database
135
+ # consistency
136
+ #
137
+ # @option options [Boolean, nil] :no_references (nil) skip reference checking
138
+ #
139
+ # @return [Git::FsckResult] the objects flagged by fsck, categorized by status
140
+ #
141
+ # @raise [ArgumentError] when unsupported options are provided
142
+ #
143
+ # @raise [Git::FailedError] when git exits outside the allowed range (exit
144
+ # code > 7)
145
+ #
146
+ def fsck(*objects, **)
147
+ SharedPrivate.assert_valid_opts!(FSCK_ALLOWED_OPTS, **)
148
+ result = Git::Commands::Fsck.new(@execution_context).call(*objects, **, no_progress: true)
149
+ Git::Parsers::Fsck.parse(result.stdout)
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/log'
4
+ require 'git/log'
5
+ require 'git/repository/shared_private'
6
+
7
+ module Git
8
+ class Repository
9
+ # Facade methods for querying commit history
10
+ #
11
+ # Included by {Git::Repository}.
12
+ #
13
+ # @api public
14
+ #
15
+ module Logging
16
+ FULL_LOG_COMMITS_ALLOWED_OPTS = %i[
17
+ count all cherry since until grep author between object path_limiter skip merges
18
+ ].freeze
19
+ private_constant :FULL_LOG_COMMITS_ALLOWED_OPTS
20
+
21
+ # Returns commits within the given revision range
22
+ #
23
+ # @overload full_log_commits(opts = {})
24
+ #
25
+ # @example Return commits from all refs
26
+ # repo.full_log_commits(all: true).first['sha']
27
+ # #=> "a1b2c3d4..."
28
+ #
29
+ # @example Return commits between two revisions
30
+ # repo.full_log_commits(between: ['v1.0.0', 'HEAD']).map { |c| c['sha'] }
31
+ # #=> ["d4e5f6...", "a1b2c3..."]
32
+ #
33
+ # @param opts [Hash] options for the log query
34
+ #
35
+ # @option opts [Integer, nil] :count (nil) maximum number of commits to return
36
+ #
37
+ # @option opts [Boolean, nil] :all (nil) include commits reachable from any ref
38
+ #
39
+ # @option opts [Boolean, nil] :cherry (nil) omit commits equivalent to cherry-picked commits
40
+ #
41
+ # @option opts [String] :since (nil) include commits newer than this date expression
42
+ #
43
+ # @option opts [String] :until (nil) include commits older than this date expression
44
+ #
45
+ # @option opts [String] :grep (nil) only include commits whose message matches this pattern
46
+ #
47
+ # @option opts [String] :author (nil) only include commits whose author matches this pattern
48
+ #
49
+ # @option opts [Array<String>] :between (nil) revision range as two commit-ish values
50
+ #
51
+ # When both `:between` and `:object` are provided, `:between` takes precedence.
52
+ #
53
+ # @option opts [String] :object (nil) single revision range expression for git log
54
+ #
55
+ # Ignored when `:between` is provided.
56
+ #
57
+ # @option opts [String, Pathname, Array<String, Pathname>] :path_limiter (nil)
58
+ # only include commits that impact files from the specified path(s)
59
+ #
60
+ # @option opts [Integer] :skip (nil) skip this many commits before output
61
+ #
62
+ # @option opts [Boolean, nil] :merges (nil) include only merge commits
63
+ #
64
+ # @return [Array<Hash>] the parsed raw log output for each commit
65
+ #
66
+ # @raise [ArgumentError] if unsupported options are provided
67
+ #
68
+ # @raise [ArgumentError] if `:count` is not an Integer
69
+ #
70
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
71
+ #
72
+ # @see https://git-scm.com/docs/git-log git-log
73
+ #
74
+ def full_log_commits(opts = {})
75
+ SharedPrivate.assert_valid_opts!(FULL_LOG_COMMITS_ALLOWED_OPTS, **opts)
76
+ Private.validate_log_count_option!(opts)
77
+ Private.validate_log_between_option!(opts)
78
+
79
+ call_opts = Private.log_base_call_options(opts, skip: opts[:skip], merges: opts[:merges])
80
+ revision_range_args = Private.log_revision_range_args(opts)
81
+ Private.run_log_command(@execution_context, revision_range_args, call_opts)
82
+ end
83
+
84
+ # Returns a new {Git::Log} query builder scoped to this repository
85
+ #
86
+ # @example Build a log query and execute it
87
+ # results = repo.log(50).author('Alice').since('2 weeks ago').execute
88
+ # results.each { |commit| puts commit.sha }
89
+ #
90
+ # @param count [Integer, Symbol, nil] the maximum number of commits to return,
91
+ # or `:all` / `nil` to return all commits; passed directly to {Git::Log#initialize}
92
+ #
93
+ # @return [Git::Log] a new log query builder
94
+ #
95
+ # @see Git::Log
96
+ #
97
+ def log(count = 30)
98
+ Git::Log.new(self, count)
99
+ end
100
+
101
+ # Internal helpers for {Logging} that should not be mixed into
102
+ # {Git::Repository} instances
103
+ #
104
+ # @api private
105
+ #
106
+ module Private
107
+ module_function
108
+
109
+ def validate_log_count_option!(opts)
110
+ return if opts[:count].nil? || opts[:count].is_a?(Integer)
111
+
112
+ raise ArgumentError, "The log count option must be an Integer but was #{opts[:count].inspect}"
113
+ end
114
+
115
+ def validate_log_between_option!(opts)
116
+ between = opts[:between]
117
+ return if between.nil?
118
+ return if between.is_a?(Array) && between.length == 2 && between.none?(&:nil?)
119
+
120
+ raise ArgumentError,
121
+ "The log between option must be an Array with exactly two non-nil values but was #{between.inspect}"
122
+ end
123
+
124
+ def log_revision_range_args(opts)
125
+ if opts[:between]
126
+ ["#{opts[:between][0]}..#{opts[:between][1]}"]
127
+ elsif opts[:object].is_a?(String)
128
+ [opts[:object]]
129
+ else
130
+ []
131
+ end
132
+ end
133
+
134
+ def log_base_call_options(opts, extra = {})
135
+ {
136
+ all: opts[:all],
137
+ cherry: opts[:cherry],
138
+ since: opts[:since],
139
+ until: opts[:until],
140
+ grep: opts[:grep],
141
+ author: opts[:author],
142
+ max_count: opts[:count],
143
+ path: opts[:path_limiter] ? Array(opts[:path_limiter]) : nil
144
+ }.merge(extra).compact
145
+ end
146
+
147
+ def run_log_command(execution_context, revision_range_args, call_opts)
148
+ log_or_empty_on_unborn do
149
+ result = Git::Commands::Log.new(execution_context).call(
150
+ *revision_range_args,
151
+ no_color: true,
152
+ pretty: 'raw',
153
+ **call_opts
154
+ )
155
+ RawLogParser.new(result.stdout.split("\n")).parse
156
+ end
157
+ end
158
+
159
+ def log_or_empty_on_unborn
160
+ yield
161
+ rescue Git::FailedError => e
162
+ raise unless e.result.status.exitstatus == 128 &&
163
+ e.result.stderr =~ /does not have any commits yet/
164
+
165
+ []
166
+ end
167
+
168
+ # Parser for `git log --pretty=raw` output into commit hashes
169
+ #
170
+ # @api private
171
+ #
172
+ class RawLogParser
173
+ def initialize(lines)
174
+ @lines = lines
175
+ @commits = []
176
+ @current_commit = nil
177
+ @in_message = false
178
+ @last_metadata_key = nil
179
+ end
180
+
181
+ # Parse raw `git log --pretty=raw` lines into commit hashes
182
+ #
183
+ # @return [Array<Hash>] the parsed commits in command output order
184
+ #
185
+ def parse
186
+ @lines.each { |line| process_line(line.chomp) }
187
+ finalize_commit
188
+ @commits
189
+ end
190
+
191
+ private
192
+
193
+ def process_line(line)
194
+ if line.empty?
195
+ @in_message = !@in_message
196
+ return
197
+ end
198
+
199
+ @in_message = false if @in_message && !line.start_with?(' ')
200
+
201
+ @in_message ? process_message_line(line) : process_metadata_line(line)
202
+ end
203
+
204
+ def process_message_line(line)
205
+ @current_commit['message'] << "#{line[4..]}\n"
206
+ end
207
+
208
+ def process_metadata_line(line)
209
+ if line.start_with?(' ') && @last_metadata_key
210
+ @current_commit[@last_metadata_key] << "\n#{line[1..]}"
211
+ return
212
+ end
213
+
214
+ key, *value = line.split
215
+ value = value.join(' ')
216
+ @last_metadata_key = nil
217
+ dispatch_metadata_key(key, value)
218
+ end
219
+
220
+ def dispatch_metadata_key(key, value)
221
+ case key
222
+ when 'commit'
223
+ start_new_commit(value)
224
+ when 'parent'
225
+ @current_commit['parent'] << value
226
+ else
227
+ @current_commit[key] = value
228
+ @last_metadata_key = key
229
+ end
230
+ end
231
+
232
+ def start_new_commit(sha)
233
+ finalize_commit
234
+ @current_commit = { 'sha' => sha, 'message' => +'', 'parent' => [] }
235
+ @last_metadata_key = nil
236
+ end
237
+
238
+ def finalize_commit
239
+ @commits << @current_commit if @current_commit
240
+ end
241
+ end
242
+ private_constant :RawLogParser
243
+ end
244
+ private_constant :Private
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,295 @@
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
+ # and finding common ancestors between commits
15
+ #
16
+ # Included by {Git::Repository}.
17
+ #
18
+ # @api public
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 [String, Array<String>, #to_s] the branch or branches to merge
52
+ # into the current branch
53
+ #
54
+ # When an Array is given, an octopus merge is performed; a {Git::Branch}
55
+ # object 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
+ # Find common ancestor commit(s) for use in a merge
101
+ #
102
+ # @example Find the common ancestor of two branches
103
+ # repo.merge_base('main', 'feature') #=> ["abc123def456..."]
104
+ #
105
+ # @example Find all common ancestors of two branches
106
+ # repo.merge_base('branch-a', 'branch-b', all: true)
107
+ #
108
+ # @example Find the fork point of a branch (consults the reflog)
109
+ # repo.merge_base('main', 'feature', fork_point: true)
110
+ #
111
+ # @example Find independent commits not reachable from each other
112
+ # repo.merge_base('abc1234', 'main', 'feature', independent: true)
113
+ #
114
+ # @overload merge_base(*commits, options = {})
115
+ #
116
+ # @param commits [Array<String>] two or more commit SHAs, branch names,
117
+ # or refs to find the common ancestor(s) of
118
+ #
119
+ # @param options [Hash] merge-base options
120
+ #
121
+ # @option options [Boolean, nil] :octopus (nil) compute the best common
122
+ # ancestor for an n-way merge (intersection of all merge bases)
123
+ #
124
+ # @option options [Boolean, nil] :independent (nil) list commits not
125
+ # reachable from any other; useful for finding minimal merge points
126
+ #
127
+ # @option options [Boolean, nil] :fork_point (nil) find the fork point
128
+ # where a branch diverged from another, consulting the reflog
129
+ #
130
+ # @option options [Boolean, nil] :all (nil) output all merge bases instead
131
+ # of just the first when multiple equally good bases exist
132
+ #
133
+ # @return [Array<String>] commit SHAs of the common ancestor(s); empty
134
+ # when no common ancestor exists or `--fork-point` finds none
135
+ #
136
+ # @raise [ArgumentError] when unsupported options are provided
137
+ #
138
+ # @raise [Git::FailedError] when `git merge-base` exits outside the
139
+ # allowed range (exit code > 1)
140
+ #
141
+ def merge_base(*args)
142
+ opts = args.last.is_a?(Hash) ? args.pop : {}
143
+ SharedPrivate.assert_valid_opts!(MERGE_BASE_ALLOWED_OPTS, **opts)
144
+ result = Git::Commands::MergeBase.new(@execution_context).call(*args, **opts)
145
+ result.stdout.lines.map(&:strip).reject(&:empty?)
146
+ end
147
+
148
+ # Iterate over files with merge conflicts, yielding conflict details for each
149
+ #
150
+ # For each unmerged file, the staged content for both sides of the conflict
151
+ # (stage 2 "ours" and stage 3 "theirs") is written to temporary files whose
152
+ # paths are yielded alongside the file path. The temporary files are deleted
153
+ # automatically when the block returns.
154
+ #
155
+ # @example Inspect conflicting files
156
+ # repo.each_conflict do |file, your_version, their_version|
157
+ # puts "Conflict in #{file}"
158
+ # puts "Your version:"
159
+ # puts File.read(your_version)
160
+ # puts "Their version:"
161
+ # puts File.read(their_version)
162
+ # end
163
+ #
164
+ # @return [Array<String>] the list of unmerged file paths
165
+ #
166
+ # @raise [Git::FailedError] when `git diff --cached` exits with a non-zero status
167
+ #
168
+ # @yield [file, your_version, their_version] passes conflict details for
169
+ # each unmerged file
170
+ #
171
+ # @yieldparam file [String] path to the conflicting file, relative to the
172
+ # working tree
173
+ #
174
+ # @yieldparam your_version [String] path to a temporary file containing the
175
+ # stage-2 (ours) content for the conflicting file
176
+ #
177
+ # @yieldparam their_version [String] path to a temporary file containing the
178
+ # stage-3 (theirs) content for the conflicting file
179
+ #
180
+ # @yieldreturn [void]
181
+ #
182
+ def each_conflict
183
+ Private.unmerged_paths(@execution_context).each do |file_path|
184
+ Private.write_staged_file(@execution_context, file_path, 2) do |your_file|
185
+ Private.write_staged_file(@execution_context, file_path, 3) do |their_file|
186
+ yield(file_path, your_file.path, their_file.path)
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ # Option keys accepted by {#revert}
193
+ #
194
+ # Derived from the 4.x option map for `Git::Lib#revert`.
195
+ REVERT_ALLOWED_OPTS = %i[no_edit].freeze
196
+ private_constant :REVERT_ALLOWED_OPTS
197
+
198
+ # Revert one or more existing commits by creating new commits that undo
199
+ # the changes those commits introduced
200
+ #
201
+ # The working tree must be clean before calling this method. By default
202
+ # the editor is suppressed (`--no-edit`) so the commit message is taken
203
+ # from git's default revert message without prompting.
204
+ #
205
+ # @example Revert the most recent commit
206
+ # repo.revert('HEAD')
207
+ #
208
+ # @example Revert a specific commit by SHA
209
+ # repo.revert('abc1234')
210
+ #
211
+ # @example Revert a range of commits
212
+ # repo.revert('HEAD~3..HEAD~1')
213
+ #
214
+ # @example Revert without suppressing the editor
215
+ # repo.revert('HEAD', no_edit: false)
216
+ #
217
+ # @param commitish [String, nil] the commit, ref, or rev range to revert;
218
+ # see `gitrevisions(7)` for accepted forms; defaults to `'HEAD'` when
219
+ # `nil`
220
+ #
221
+ # @param opts [Hash] additional options forwarded to `git revert`
222
+ #
223
+ # @option opts [Boolean, nil] :no_edit (true) suppress the commit-message
224
+ # editor (`--no-edit`); pass `false` to open the editor
225
+ #
226
+ # @return [String] git's stdout from the revert command
227
+ #
228
+ # @raise [ArgumentError] when unsupported options are provided
229
+ #
230
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
231
+ #
232
+ def revert(commitish = nil, opts = {})
233
+ commitish = 'HEAD' if commitish.nil?
234
+ SharedPrivate.assert_valid_opts!(REVERT_ALLOWED_OPTS, **opts)
235
+ opts = { no_edit: true }.merge(opts)
236
+ Git::Commands::Revert::Start.new(@execution_context).call(commitish, **opts).stdout
237
+ end
238
+
239
+ # Private helpers local to {Git::Repository::Merging}
240
+ #
241
+ # @api private
242
+ module Private
243
+ # Tempfile name prefixes for staged content, keyed by git stage index
244
+ STAGE_PREFIXES = { 2 => 'YOUR-', 3 => 'THEIR-' }.freeze
245
+
246
+ module_function
247
+
248
+ # Returns the list of file paths with unresolved merge conflicts
249
+ #
250
+ # @param execution_context [Git::ExecutionContext] the execution context
251
+ # used to run git commands
252
+ #
253
+ # @return [Array<String>] unmerged file paths
254
+ #
255
+ # @api private
256
+ #
257
+ def unmerged_paths(execution_context)
258
+ result = Git::Commands::Diff.new(execution_context).call(cached: true)
259
+ result.stdout.split("\n").filter_map do |line|
260
+ ::Regexp.last_match(1) if line =~ /^\* Unmerged path (.*)/
261
+ end
262
+ end
263
+
264
+ # Creates a Tempfile with the staged content for `file_path` at `stage`
265
+ # and yields the open IO object to the block
266
+ #
267
+ # @param execution_context [Git::ExecutionContext] the execution context
268
+ # used to run git commands
269
+ #
270
+ # @param file_path [String] repository-relative path to the conflicting file
271
+ #
272
+ # @param stage [Integer] git stage index (2 = ours, 3 = theirs)
273
+ #
274
+ # @yield [f] yields the open Tempfile containing the staged content
275
+ #
276
+ # @yieldparam f [Tempfile] open IO object for the staged content
277
+ #
278
+ # @yieldreturn [void]
279
+ #
280
+ # @return [void]
281
+ #
282
+ # @api private
283
+ #
284
+ def write_staged_file(execution_context, file_path, stage)
285
+ Tempfile.create([STAGE_PREFIXES[stage], File.basename(file_path)]) do |f|
286
+ Git::Commands::Show.new(execution_context).call(":#{stage}:#{file_path}", out: f)
287
+ f.flush
288
+ yield f
289
+ end
290
+ end
291
+ end
292
+ private_constant :Private
293
+ end
294
+ end
295
+ end