git 4.4.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 -67
  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 -73
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -17
  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 +485 -420
  52. data/lib/git/branch.rb +117 -59
  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 -1084
  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 +226 -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 +117 -56
  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
data/lib/git/base.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'logger'
4
+ require 'pathname'
5
+ require 'git/repository/path_resolver'
4
6
 
5
7
  module Git
6
8
  # The main public interface for interacting with Git commands
@@ -14,22 +16,27 @@ module Git
14
16
  class Base
15
17
  # (see Git.bare)
16
18
  def self.bare(git_dir, options = {})
17
- normalize_paths(options, default_repository: git_dir, bare: true)
18
- new(options)
19
+ paths = Git::Repository::PathResolver.resolve_paths(repository: git_dir, bare: true)
20
+ new(options.merge(paths))
19
21
  end
20
22
 
21
23
  # (see Git.clone)
22
24
  def self.clone(repository_url, directory, options = {})
23
25
  lib_options = {}
24
26
  lib_options[:git_ssh] = options[:git_ssh] if options.key?(:git_ssh)
25
- new_options = LibImpl.new(lib_options, options[:log]).clone(repository_url, directory, options)
26
- normalize_paths(new_options, bare: options[:bare] || options[:mirror])
27
- new(new_options)
27
+ clone_result = Git::Lib.new(lib_options, options[:log]).clone(repository_url, directory, options)
28
+ bare = options[:bare] || options[:mirror]
29
+ paths = Git::Repository::PathResolver.resolve_paths(
30
+ working_directory: clone_result[:working_directory],
31
+ repository: clone_result[:repository],
32
+ bare: bare
33
+ )
34
+ new(options.merge(paths))
28
35
  end
29
36
 
30
37
  # (see Git.default_branch)
31
38
  def self.repository_default_branch(repository, options = {})
32
- LibImpl.new(nil, options[:log]).repository_default_branch(repository)
39
+ Git::Lib.new(nil, options[:log]).repository_default_branch(repository)
33
40
  end
34
41
 
35
42
  # Returns (and initialize if needed) a Git::Config instance
@@ -39,96 +46,32 @@ module Git
39
46
  @config ||= Config.new
40
47
  end
41
48
 
49
+ # @deprecated Use {Git.git_version} instead, which returns a {Git::Version} (not an Array).
50
+ # For the legacy array shape, call: `Git.git_version.to_a`
51
+ #
42
52
  def self.binary_version(binary_path)
43
- parse_version_string(execute_git_version(binary_path))
44
- end
45
-
46
- private_class_method def self.execute_git_version(binary_path)
47
- bootstrap_command_line(binary_path).run('version', merge: true).stdout
48
- rescue Git::CommandLineError => e
49
- raise "Failed to get git version: #{e.result.status}\n#{e.result.stdout}"
50
- rescue Errno::ENOENT, ProcessExecuter::SpawnError
51
- raise "Failed to get git version: #{binary_path} not found"
52
- end
53
-
54
- private_class_method def self.parse_version_string(raw_string)
55
- version_match = raw_string.match(/\d+(\.\d+)+/)
56
- return [0, 0, 0] unless version_match
57
-
58
- version_parts = version_match[0].split('.').map(&:to_i)
59
- version_parts.fill(0, version_parts.length...3)
60
- end
61
-
62
- # (see Git.init)
63
- def self.init(directory = '.', options = {})
64
- normalize_paths(options, default_working_directory: directory, default_repository: directory,
65
- bare: options[:bare])
66
-
67
- init_options = {
68
- bare: options[:bare],
69
- initial_branch: options[:initial_branch]
70
- }
71
-
72
- directory = options[:bare] ? options[:repository] : options[:working_directory]
73
- FileUtils.mkdir_p(directory)
74
-
75
- # TODO: this dance seems awkward: this creates a Git::Lib so we can call
76
- # init so we can create a new Git::Base which in turn (ultimately)
77
- # creates another/different Git::Lib.
78
- #
79
- # TODO: maybe refactor so this Git::Bare.init does this:
80
- # self.new(opts).init(init_opts) and move all/some of this code into
81
- # Git::Bare#init. This way the init method can be called on any
82
- # repository you have a Git::Base instance for. This would not
83
- # change the existing interface (other than adding to it).
84
- #
85
- LibImpl.new(options).init(init_options)
86
-
87
- new(options)
88
- end
89
-
90
- def self.root_of_worktree(working_dir)
91
- raise ArgumentError, "'#{working_dir}' does not exist" unless Dir.exist?(working_dir)
92
-
93
- execute_rev_parse_toplevel(working_dir)
94
- end
95
-
96
- private_class_method def self.execute_rev_parse_toplevel(working_dir)
97
- bootstrap_command_line(Git::Base.config.binary_path).run(
98
- 'rev-parse', '--show-toplevel', chdir: File.expand_path(working_dir), merge: true, chomp: true
99
- ).stdout
100
- rescue Git::CommandLineError
101
- raise ArgumentError, "'#{working_dir}' is not in a git working tree"
102
- rescue Errno::ENOENT, ProcessExecuter::SpawnError
103
- raise ArgumentError, 'Failed to find the root of the worktree: git binary not found'
53
+ Git::Deprecation.warn(
54
+ 'Git::Base.binary_version is deprecated and will be removed in 6.0. ' \
55
+ 'Use Git.git_version instead, which returns a Git::Version ' \
56
+ '(not an Array). For the legacy array shape, call: Git.git_version.to_a'
57
+ )
58
+ Git.git_version(binary_path).to_a
104
59
  end
105
60
 
106
- # A command line for the git commands that run before a repository is known
61
+ # Find the root of the working tree that contains `working_dir`
107
62
  #
108
- # `Open3` is deliberately not used here. Windows has no fork, so
109
- # `Process.spawn` implements a redirect of the child's stdin by redirecting
110
- # the *parent's* stdin and restoring it afterward, which replaces the
111
- # process's stdin handle. That leaves a console REPL such as irb or pry
112
- # unable to read input for the rest of the session. `Git::CommandLine`
113
- # redirects only stdout and stderr, so stdin is never disturbed.
63
+ # Delegates to {Git::Repository::PathResolver.root_of_worktree}, using the
64
+ # global config for `binary_path` and `git_ssh`.
114
65
  #
115
- # @see https://github.com/ruby-git/ruby-git/issues/840 issue 840
66
+ # @param working_dir [String] a path inside the working tree
116
67
  #
117
- # @param binary_path [String] the path to the git binary to run
68
+ # @return [String] the absolute path to the root of the working tree
118
69
  #
119
- # @return [Git::CommandLine] a command line that runs `binary_path` with
120
- # `-c core.quotePath=true -c color.ui=false`
70
+ # @raise [ArgumentError] if `working_dir` does not exist or is not inside a
71
+ # git working tree
121
72
  #
122
- # Those are the options the `Open3` calls this replaced passed, kept as they were
123
- # so these two commands behave exactly as before. Neither `git version` nor
124
- # `git rev-parse --show-toplevel` emits colored output, so the remaining `color.*`
125
- # settings in {Git::Lib}'s `STATIC_GLOBAL_OPTS` are not needed here.
126
- #
127
- # @api private
128
- #
129
- private_class_method def self.bootstrap_command_line(binary_path)
130
- Git::CommandLine.new({}, binary_path, ['-c', 'core.quotePath=true', '-c', 'color.ui=false'],
131
- Logger.new(nil))
73
+ def self.root_of_worktree(working_dir)
74
+ Git::Repository::PathResolver.root_of_worktree(working_dir)
132
75
  end
133
76
 
134
77
  # (see Git.open)
@@ -137,9 +80,13 @@ module Git
137
80
 
138
81
  working_dir = root_of_worktree(working_dir) unless options[:repository]
139
82
 
140
- normalize_paths(options, default_working_directory: working_dir)
83
+ paths = Git::Repository::PathResolver.resolve_paths(
84
+ working_directory: working_dir,
85
+ repository: options[:repository],
86
+ index: options[:index]
87
+ )
141
88
 
142
- new(options)
89
+ new(options.merge(paths))
143
90
  end
144
91
 
145
92
  # Create an object that executes Git commands in the context of a working
@@ -148,51 +95,100 @@ module Git
148
95
  # @param [Hash] options The options for this command (see list of valid
149
96
  # options below)
150
97
  #
151
- # @option options [Pathname] :working_dir the path to the root of the working
152
- # directory. Should be `nil` if executing commands on a bare repository.
98
+ # @option options [Pathname] :working_directory the path to the root of the working
99
+ # directory or `nil` if executing commands on a bare repository
153
100
  #
154
101
  # @option options [Pathname] :repository used to specify a non-standard path to
155
- # the repository directory. The default is `"#{working_dir}/.git"`.
102
+ # the repository directory
103
+ #
104
+ # The default is `"<working_directory>/.git"`.
156
105
  #
157
106
  # @option options [Pathname] :index used to specify a non-standard path to an
158
- # index file. The default is `"#{working_dir}/.git/index"`
107
+ # index file
159
108
  #
160
- # @option options [Logger] :log A logger to use for Git operations. Git
161
- # commands are logged at the `:info` level. Additional logging is done
109
+ # The default is `"<working_directory>/.git/index"`
110
+ #
111
+ # @option options [Logger] :log A logger to use for Git operations
112
+ #
113
+ # Git commands are logged at the `:info` level. Additional logging is done
162
114
  # at the `:debug` level.
163
115
  #
164
- # @option options [String, nil] :git_ssh Path to a custom SSH executable or script.
116
+ # @option options [String, nil] :git_ssh Path to a custom SSH executable or script
117
+ #
165
118
  # Controls how SSH is configured for this {Git::Base} instance:
166
119
  # - If this option is not provided, the global Git::Base.config.git_ssh setting is used.
167
120
  # - If this option is explicitly set to nil, SSH is disabled for this instance.
168
121
  # - If this option is a non-empty String, that value is used as the SSH command for
169
122
  # this instance, overriding the global Git::Base.config.git_ssh setting.
170
123
  #
171
- # @return [Git::Base] an object that can execute git commands in the context
172
- # of the opened working copy or bare repository
124
+ # @option options [String, :use_global_config] :binary_path Path to the git binary
125
+ #
126
+ # Controls which git binary is used for commands routed through
127
+ # {Git::ExecutionContext} (i.e., commands already migrated to
128
+ # +Git::Commands::*+ classes). Commands still delegating through +Git::Lib+
129
+ # continue to use the global `Git::Base.config.binary_path` setting.
130
+ #
131
+ # This limitation will be resolved when the architectural migration to
132
+ # +Git::Repository+ is complete.
133
+ #
134
+ # - If this option is not provided, the global Git::Base.config.binary_path setting is used.
135
+ # - If this option is a String, that value is used as the git binary path for
136
+ # migrated commands, overriding the global Git::Base.config.binary_path setting.
137
+ # - Passing `nil` raises ArgumentError — there is no "unset the binary" semantic.
138
+ #
139
+ # @return [Git::Base] an object that can execute git commands on a working copy or
140
+ # bare repository
141
+ #
142
+ # @raise [ArgumentError] if `binary_path` is `nil`
173
143
  #
174
144
  def initialize(options = {})
175
- options = default_paths(options)
176
145
  setup_logger(options[:log])
177
146
  @git_ssh = options.key?(:git_ssh) ? options[:git_ssh] : :use_global_config
147
+ if options.key?(:binary_path)
148
+ raise ArgumentError, 'binary_path must not be nil' if options[:binary_path].nil?
149
+
150
+ @binary_path = options[:binary_path]
151
+ else
152
+ @binary_path = :use_global_config
153
+ end
178
154
  initialize_components(options)
179
155
  end
180
156
 
181
- # Update the index from the current worktree to prepare the for the next commit
157
+ # Update the index from the current worktree to prepare for the next commit
182
158
  #
183
- # @example
184
- # lib.add('path/to/file')
185
- # lib.add(['path/to/file1','path/to/file2'])
186
- # lib.add(all: true)
159
+ # @overload add(paths = '.', **options)
160
+ #
161
+ # @example Stage all changed files
162
+ # git.add
163
+ #
164
+ # @example Stage a specific file
165
+ # git.add('path/to/file.rb')
187
166
  #
188
- # @param [String, Array<String>] paths a file or files to be added to the repository (relative to the worktree root)
189
- # @param [Hash] options
167
+ # @example Stage multiple files
168
+ # git.add(['path/to/file1.rb', 'path/to/file2.rb'])
190
169
  #
191
- # @option options [Boolean] :all Add, modify, and remove index entries to match the worktree
192
- # @option options [Boolean] :force Allow adding otherwise ignored files
170
+ # @example Stage all changes including deletions
171
+ # git.add(all: true)
193
172
  #
194
- def add(paths = '.', **options)
195
- lib.add(paths, options)
173
+ # @param paths [String, Array<String>] a file or files to add (relative to
174
+ # the worktree root); defaults to `'.'` (all files)
175
+ #
176
+ # @param options [Hash] options for the add command
177
+ #
178
+ # @option options [Boolean, nil] :all (nil) add, modify, and remove index
179
+ # entries to match the worktree
180
+ #
181
+ # @option options [Boolean, nil] :force (nil) allow adding otherwise ignored
182
+ # files
183
+ #
184
+ # @return [String] git's stdout from the add
185
+ #
186
+ # @raise [ArgumentError] if unsupported options are provided
187
+ #
188
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
189
+ #
190
+ def add(paths = '.', **)
191
+ facade_repository.add(paths, **)
196
192
  end
197
193
 
198
194
  # adds a new remote to this repository
@@ -206,9 +202,7 @@ module Git
206
202
  # :fetch => true
207
203
  # :track => <branch_name>
208
204
  def add_remote(name, url, opts = {})
209
- url = url.repo.to_s if url.is_a?(Git::Base)
210
- lib.remote_add(name, url, opts)
211
- Git::Remote.new(self, name)
205
+ facade_repository.add_remote(name, url, opts)
212
206
  end
213
207
 
214
208
  # changes current working directory for a block
@@ -220,42 +214,46 @@ module Git
220
214
  # @git.add
221
215
  # @git.commit('message')
222
216
  # end
223
- def chdir # :yields: the Git::Path
217
+ def chdir # :yields: the working directory Pathname
224
218
  Dir.chdir(dir.to_s) do
225
- yield dir.to_s
219
+ yield dir
226
220
  end
227
221
  end
228
222
 
229
223
  # g.config('user.name', 'Scott Chacon') # sets value
230
224
  # g.config('user.email', 'email@email.com') # sets value
231
- # g.config('user.email', 'email@email.com', file: 'path/to/custom/config) # sets value in file
225
+ # g.config('user.email', 'email@email.com', file: 'path/to/custom/config') # sets value in file
232
226
  # g.config('user.name') # returns 'Scott Chacon'
233
227
  # g.config # returns whole config hash
234
228
  def config(name = nil, value = nil, options = {})
235
- if name && value
236
- # set value
237
- lib.config_set(name, value, options)
238
- elsif name
239
- # return value
240
- lib.config_get(name)
241
- else
242
- # return hash
243
- lib.config_list
244
- end
229
+ facade_repository.config(name, value, options)
245
230
  end
246
231
 
247
- # returns a reference to the working directory
248
- # @git.dir.path
249
- # @git.dir.writeable?
232
+ # Returns a reference to the working directory
233
+ #
234
+ # @example
235
+ # @git.dir.to_s
236
+ # @git.dir.writable?
237
+ #
238
+ # @return [Pathname] the working directory path
239
+ #
250
240
  def dir
251
241
  @working_directory
252
242
  end
253
243
 
254
- # returns reference to the git index file
244
+ # Returns a reference to the git index file
245
+ #
246
+ # @return [Pathname] the index file path
247
+ #
255
248
  attr_reader :index
256
249
 
257
- # returns reference to the git repository directory
258
- # @git.dir.path
250
+ # Returns a reference to the git repository directory
251
+ #
252
+ # @example
253
+ # @git.repo.to_s
254
+ #
255
+ # @return [Pathname] the repository directory path
256
+ #
259
257
  def repo
260
258
  @repository
261
259
  end
@@ -270,40 +268,44 @@ module Git
270
268
  .sum { |file| File.stat(file).size.to_i }
271
269
  end
272
270
 
273
- def set_index(index_file, check = nil, must_exist: nil)
271
+ private
272
+
273
+ def deprecate_check_argument(check, must_exist)
274
274
  unless check.nil?
275
275
  Git::Deprecation.warn(
276
276
  'The "check" argument is deprecated and will be removed in a future version. ' \
277
277
  'Use "must_exist:" instead.'
278
278
  )
279
279
  end
280
-
281
280
  # default is true
282
- must_exist = must_exist.nil? && check.nil? ? true : must_exist | check
283
-
284
- @lib = nil
285
- @index = Git::Index.new(index_file.to_s, must_exist:)
281
+ must_exist.nil? && check.nil? ? true : must_exist | check
286
282
  end
287
283
 
288
- def set_working(work_dir, check = nil, must_exist: nil)
289
- unless check.nil?
290
- Git::Deprecation.warn(
291
- 'The "check" argument is deprecated and will be removed in a future version. ' \
292
- 'Use "must_exist:" instead.'
293
- )
284
+ def validate_path(path, must_exist)
285
+ Pathname.new(File.expand_path(path.to_s)).tap do |expanded_path|
286
+ raise ArgumentError, "path does not exist: #{expanded_path}" if must_exist && !expanded_path.exist?
294
287
  end
288
+ end
295
289
 
296
- # default is true
297
- must_exist = must_exist.nil? && check.nil? ? true : must_exist | check
290
+ public
298
291
 
292
+ def set_index(index_file, check = nil, must_exist: nil)
293
+ must_exist = deprecate_check_argument(check, must_exist)
299
294
  @lib = nil
300
- @working_directory = Git::WorkingDirectory.new(work_dir.to_s, must_exist:)
295
+ @facade_repository = nil
296
+ @index = validate_path(index_file, must_exist)
297
+ end
298
+
299
+ def set_working(work_dir, check = nil, must_exist: nil)
300
+ must_exist = deprecate_check_argument(check, must_exist)
301
+ @lib = nil
302
+ @facade_repository = nil
303
+ @working_directory = validate_path(work_dir, must_exist)
301
304
  end
302
305
 
303
306
  # returns +true+ if the branch exists locally
304
307
  def local_branch?(branch)
305
- branch_names = branches.local.map(&:name)
306
- branch_names.include?(branch)
308
+ facade_repository.local_branch?(branch)
307
309
  end
308
310
 
309
311
  def is_local_branch?(branch) # rubocop:disable Naming/PredicatePrefix
@@ -316,8 +318,7 @@ module Git
316
318
 
317
319
  # returns +true+ if the branch exists remotely
318
320
  def remote_branch?(branch)
319
- branch_names = branches.remote.map(&:name)
320
- branch_names.include?(branch)
321
+ facade_repository.remote_branch?(branch)
321
322
  end
322
323
 
323
324
  def is_remote_branch?(branch) # rubocop:disable Naming/PredicatePrefix
@@ -330,8 +331,7 @@ module Git
330
331
 
331
332
  # returns +true+ if the branch exists
332
333
  def branch?(branch)
333
- branch_names = branches.map(&:name)
334
- branch_names.include?(branch)
334
+ facade_repository.branch?(branch)
335
335
  end
336
336
 
337
337
  def is_branch?(branch) # rubocop:disable Naming/PredicatePrefix
@@ -346,10 +346,20 @@ module Git
346
346
  # actual 'git' forked system calls. At some point I hope to replace the Git::Lib
347
347
  # class with one that uses native methods or libgit C bindings
348
348
  def lib
349
- @lib ||= LibImpl.new(self, @logger)
349
+ @lib ||= Git::Lib.new(self, @logger)
350
+ end
351
+
352
+ # Returns the {Git::Repository} facade for this repository
353
+ #
354
+ # @return [Git::Repository]
355
+ # @api private
356
+ def facade_repository
357
+ @facade_repository ||= Git::Repository.new(
358
+ execution_context: Git::ExecutionContext::Repository.from_base(self, logger: @logger)
359
+ )
350
360
  end
351
361
 
352
- # Returns the per-instance git_ssh configuration value.
362
+ # Returns the per-instance git_ssh configuration value
353
363
  #
354
364
  # This may be:
355
365
  # * a [String] path when an explicit git_ssh command has been configured
@@ -360,6 +370,16 @@ module Git
360
370
  # @api private
361
371
  attr_reader :git_ssh
362
372
 
373
+ # Returns the per-instance git binary path configuration value
374
+ #
375
+ # This may be:
376
+ # * a [String] path when an explicit binary path has been configured
377
+ # * the Symbol `:use_global_config` when this instance is using the global config
378
+ #
379
+ # @return [String, Symbol] the binary_path configuration value for this instance
380
+ # @api private
381
+ attr_reader :binary_path
382
+
363
383
  # Run a grep for 'string' on the HEAD of the git repository
364
384
  #
365
385
  # @example Limit grep's scope by calling grep() from a specific object:
@@ -378,9 +398,9 @@ module Git
378
398
  # of paths to limit the search to or nil for no limit
379
399
  # @param opts [Hash] options to pass to the underlying `git grep` command
380
400
  #
381
- # @option opts [Boolean] :ignore_case (false) ignore case when matching
382
- # @option opts [Boolean] :invert_match (false) select non-matching lines
383
- # @option opts [Boolean] :extended_regexp (false) use extended regular expressions
401
+ # @option opts [Boolean, nil] :ignore_case (nil) ignore case when matching
402
+ # @option opts [Boolean, nil] :invert_match (nil) select non-matching lines
403
+ # @option opts [Boolean, nil] :extended_regexp (nil) use extended regular expressions
384
404
  # @option opts [String] :object (HEAD) the object to search from
385
405
  #
386
406
  # @return [Hash<String, Array>] a hash of arrays
@@ -393,30 +413,38 @@ module Git
393
413
  # ```
394
414
  #
395
415
  def grep(string, path_limiter = nil, opts = {})
396
- object('HEAD').grep(string, path_limiter, opts)
416
+ opts = opts.merge(object: facade_repository.rev_parse('HEAD')) unless opts.key?(:object)
417
+ facade_repository.grep(string, path_limiter, opts)
397
418
  end
398
419
 
399
420
  # List the files in the worktree that are ignored by git
400
- # @return [Array<String>] the list of ignored files relative to teh root of the worktree
421
+ # @return [Array<String>] the list of ignored files relative to the root of the worktree
401
422
  #
402
423
  def ignored_files
403
- lib.ignored_files
424
+ facade_repository.ignored_files
404
425
  end
405
426
 
406
427
  # removes file(s) from the git repository
407
428
  def rm(path = '.', opts = {})
408
- lib.rm(path, opts)
429
+ facade_repository.rm(path, opts)
409
430
  end
410
431
 
411
432
  alias remove rm
412
433
 
413
434
  # resets the working directory to the provided commitish
414
435
  def reset(commitish = nil, opts = {})
415
- lib.reset(commitish, opts)
436
+ facade_repository.reset(commitish, **opts)
416
437
  end
417
438
 
418
439
  # resets the working directory to the commitish with '--hard'
440
+ #
441
+ # @deprecated Use {#reset} with `hard: true` instead.
442
+ #
419
443
  def reset_hard(commitish = nil, opts = {})
444
+ Git::Deprecation.warn(
445
+ 'Git::Base#reset_hard is deprecated and will be removed in a future version. ' \
446
+ 'Use Git::Base#reset(commitish, hard: true) instead.'
447
+ )
420
448
  opts = { hard: true }.merge(opts)
421
449
  lib.reset(commitish, opts)
422
450
  end
@@ -429,7 +457,7 @@ module Git
429
457
  # :ff
430
458
  #
431
459
  def clean(opts = {})
432
- lib.clean(opts)
460
+ facade_repository.clean(opts)
433
461
  end
434
462
 
435
463
  # returns the most recent tag that is reachable from a commit
@@ -458,7 +486,7 @@ module Git
458
486
  # :no_edit
459
487
  #
460
488
  def revert(commitish = nil, opts = {})
461
- lib.revert(commitish, opts)
489
+ facade_repository.revert(commitish, **opts)
462
490
  end
463
491
 
464
492
  # commits all pending changes in the index file to the git repository
@@ -470,69 +498,71 @@ module Git
470
498
  # :author
471
499
  #
472
500
  def commit(message, opts = {})
473
- lib.commit(message, opts)
501
+ facade_repository.commit(message, **opts)
474
502
  end
475
503
 
476
504
  # commits all pending changes in the index file to the git repository,
477
505
  # but automatically adds all modified files without having to explicitly
478
506
  # calling @git.add() on them.
479
507
  def commit_all(message, opts = {})
480
- opts = { add_all: true }.merge(opts)
481
- lib.commit(message, opts)
508
+ facade_repository.commit_all(message, **opts)
482
509
  end
483
510
 
484
511
  # checks out a branch as the new git working directory
485
512
  def checkout(*, **)
486
- lib.checkout(*, **)
513
+ facade_repository.checkout(*, **)
487
514
  end
488
515
 
489
516
  # checks out an old version of a file
490
517
  def checkout_file(version, file)
491
- lib.checkout_file(version, file)
518
+ facade_repository.checkout_file(version, file)
492
519
  end
493
520
 
494
521
  # fetches changes from a remote branch - this does not modify the working directory,
495
522
  # it just gets the changes from the remote if there are any
496
523
  def fetch(remote = 'origin', opts = {})
497
- if remote.is_a?(Hash)
498
- opts = remote
499
- remote = nil
500
- end
501
- lib.fetch(remote, opts)
524
+ facade_repository.fetch(remote, opts)
502
525
  end
503
526
 
504
527
  # Push changes to a remote repository
505
528
  #
506
529
  # @overload push(remote = nil, branch = nil, options = {})
530
+ #
507
531
  # @param remote [String] the remote repository to push to
532
+ #
508
533
  # @param branch [String] the branch to push
534
+ #
509
535
  # @param options [Hash] options to pass to the push command
510
536
  #
511
- # @option opts [Boolean] :mirror (false) Push all refs under refs/heads/, refs/tags/ and refs/remotes/
512
- # @option opts [Boolean] :delete (false) Delete refs that don't exist on the remote
513
- # @option opts [Boolean] :force (false) Force updates
514
- # @option opts [Boolean] :tags (false) Push all refs under refs/tags/
515
- # @option opts [Array, String] :push_options (nil) Push options to transmit
537
+ # @option options [Boolean, nil] :mirror (nil) push all refs under refs/heads/, refs/tags/ and refs/remotes/
538
+ #
539
+ # @option options [Boolean, nil] :delete (nil) delete refs that don't exist on the remote
540
+ #
541
+ # @option options [Boolean, nil] :force (nil) force updates
542
+ #
543
+ # @option options [Boolean, nil] :tags (nil) push all refs under refs/tags/
516
544
  #
517
- # @return [Void]
545
+ # @option options [String, Array<String>] :push_option (nil) push options to transmit
546
+ #
547
+ # @return [String] the stdout output from the push command
518
548
  #
519
549
  # @raise [Git::FailedError] if the push fails
520
550
  # @raise [ArgumentError] if a branch is given without a remote
521
551
  #
522
552
  def push(*, **)
523
- lib.push(*, **)
553
+ facade_repository.push(*, **)
524
554
  end
525
555
 
526
556
  # merges one or more branches into the current working branch
527
557
  #
528
558
  # you can specify more than one branch to merge by passing an array of branches
529
559
  def merge(branch, message = 'merge', opts = {})
530
- lib.merge(branch, message, opts)
560
+ facade_repository.merge(branch, message, opts)
531
561
  end
532
562
 
533
563
  # iterates over the files which are unmerged
534
- def each_conflict(&) # :yields: file, your_version, their_version
535
- lib.conflicts(&)
564
+ def each_conflict(&)
565
+ facade_repository.each_conflict(&)
536
566
  end
537
567
 
538
568
  # Pulls the given branch from the given remote into the current branch
@@ -541,7 +571,7 @@ module Git
541
571
  # @param branch [String] the branch to pull from
542
572
  # @param opts [Hash] options to pass to the pull command
543
573
  #
544
- # @option opts [Boolean] :allow_unrelated_histories (false) Merges histories of
574
+ # @option opts [Boolean, nil] :allow_unrelated_histories (nil) merges histories of
545
575
  # two projects that started their lives independently
546
576
  # @example pulls from origin/master
547
577
  # @git.pull
@@ -550,17 +580,17 @@ module Git
550
580
  # @example pulls from upstream/develop
551
581
  # @git.pull('upstream', 'develop')
552
582
  #
553
- # @return [Void]
583
+ # @return [String] the stdout output from the pull command
554
584
  #
555
585
  # @raise [Git::FailedError] if the pull fails
556
586
  # @raise [ArgumentError] if a branch is given without a remote
557
587
  def pull(remote = nil, branch = nil, opts = {})
558
- lib.pull(remote, branch, opts)
588
+ facade_repository.pull(remote, branch, opts)
559
589
  end
560
590
 
561
591
  # returns an array of Git:Remote objects
562
592
  def remotes
563
- lib.remotes.map { |r| Git::Remote.new(self, r) }
593
+ facade_repository.remotes
564
594
  end
565
595
 
566
596
  # sets the url for a remote
@@ -569,9 +599,7 @@ module Git
569
599
  # @git.set_remote_url('scotts_git', 'git://repo.or.cz/rubygit.git')
570
600
  #
571
601
  def set_remote_url(name, url)
572
- url = url.repo.to_s if url.is_a?(Git::Base)
573
- lib.remote_set_url(name, url)
574
- Git::Remote.new(self, name)
602
+ facade_repository.set_remote_url(name, url)
575
603
  end
576
604
 
577
605
  # Configures which branches are fetched for a remote
@@ -603,24 +631,19 @@ module Git
603
631
  # the underlying git command fails
604
632
  #
605
633
  def remote_set_branches(name, *branches, add: false)
606
- branch_list = branches.flatten
607
- raise ArgumentError, 'branches are required' if branch_list.empty?
608
-
609
- lib.remote_set_branches(name, branch_list, add: add)
610
-
611
- nil
634
+ facade_repository.remote_set_branches(name, *branches, add: add)
612
635
  end
613
636
 
614
637
  # removes a remote from this repository
615
638
  #
616
639
  # @git.remove_remote('scott_git')
617
640
  def remove_remote(name)
618
- lib.remote_remove(name)
641
+ facade_repository.remove_remote(name)
619
642
  end
620
643
 
621
- # returns an array of all Git::Tag objects for this repository
644
+ # returns an array of all Git::Object::Tag objects for this repository
622
645
  def tags
623
- lib.tags.map { |r| tag(r) }
646
+ facade_repository.tags
624
647
  end
625
648
 
626
649
  # Create a new git tag
@@ -631,29 +654,53 @@ module Git
631
654
  # repo.add_tag('tag_name', {:options => 'here'})
632
655
  #
633
656
  # @param [String] name The name of the tag to add
634
- # @param [Hash] options Opstions to pass to `git tag`.
657
+ # @param [Hash] options Options to pass to `git tag`.
635
658
  # See [git-tag](https://git-scm.com/docs/git-tag) for more details.
636
- # @option options [boolean] :annotate Make an unsigned, annotated tag object
637
- # @option options [boolean] :a An alias for the `:annotate` option
638
- # @option options [boolean] :d Delete existing tag with the given names.
639
- # @option options [boolean] :f Replace an existing tag with the given name (instead of failing)
659
+ # @option options [Boolean, nil] :annotate (nil) make an unsigned, annotated tag object
660
+ # @option options [Boolean, nil] :a (nil) an alias for the `:annotate` option
661
+ # @option options [Boolean, nil] :d (nil) delete existing tag with the given name —
662
+ # deprecated; use {#delete_tag} instead (alias: `:delete`)
663
+ # @option options [Boolean, nil] :delete (nil) delete existing tag with the given name —
664
+ # deprecated; use {#delete_tag} instead (alias: `:d`)
665
+ # @option options [Boolean, nil] :f (nil) replace an existing tag with the given name (instead of failing)
640
666
  # @option options [String] :message Use the given tag message
641
667
  # @option options [String] :m An alias for the `:message` option
642
- # @option options [boolean] :s Make a GPG-signed tag.
668
+ # @option options [Boolean, nil] :s (nil) make a GPG-signed tag
643
669
  #
644
670
  def add_tag(name, *options)
645
- lib.tag(name, *options)
646
- tag(name)
671
+ facade_repository.add_tag(name, *options)
647
672
  end
648
673
 
649
674
  # deletes a tag
650
675
  def delete_tag(name)
651
- lib.tag(name, { d: true })
676
+ facade_repository.delete_tag(name)
652
677
  end
653
678
 
654
- # creates an archive file of the given tree-ish
679
+ # Creates an archive of the given tree-ish and writes it to a file
680
+ #
681
+ # @api public
682
+ #
683
+ # @param treeish [String] the commit, tag, branch, or tree to archive
684
+ #
685
+ # @param file [String, nil] destination file path; a temp file is created
686
+ # if `nil`
687
+ #
688
+ # @param opts [Hash] archive options (see {Git::Repository::ObjectOperations#archive})
689
+ #
690
+ # @return [String] the path to the written archive file
691
+ #
692
+ # @raise [ArgumentError] if unsupported options are provided
693
+ #
694
+ # @raise [ArgumentError] if `file` is an existing directory
695
+ #
696
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
697
+ #
698
+ # @example Archive HEAD to a zip file
699
+ # git.archive('HEAD', '/tmp/release.zip', format: 'zip')
700
+ # #=> "/tmp/release.zip"
701
+ #
655
702
  def archive(treeish, file = nil, opts = {})
656
- object(treeish).archive(file, opts)
703
+ facade_repository.archive(treeish, file, opts)
657
704
  end
658
705
 
659
706
  # repacks the repository
@@ -676,14 +723,14 @@ module Git
676
723
  # references in the refs namespace, and all reflogs.
677
724
  # @param [Hash] options options to pass to the underlying `git fsck` command
678
725
  #
679
- # @option options [Boolean] :unreachable print unreachable objects
680
- # @option options [Boolean] :strict enable strict checking
681
- # @option options [Boolean] :connectivity_only check only connectivity (faster)
682
- # @option options [Boolean] :root report root nodes
683
- # @option options [Boolean] :tags report tags
684
- # @option options [Boolean] :cache consider objects in the index
685
- # @option options [Boolean] :no_reflogs do not consider reflogs
686
- # @option options [Boolean] :lost_found write dangling objects to .git/lost-found
726
+ # @option options [Boolean, nil] :unreachable (nil) print unreachable objects
727
+ # @option options [Boolean, nil] :strict (nil) enable strict checking
728
+ # @option options [Boolean, nil] :connectivity_only (nil) check only connectivity (faster)
729
+ # @option options [Boolean, nil] :root (nil) report root nodes
730
+ # @option options [Boolean, nil] :tags (nil) report tags
731
+ # @option options [Boolean, nil] :cache (nil) consider objects in the index
732
+ # @option options [Boolean, nil] :no_reflogs (nil) do not consider reflogs
733
+ # @option options [Boolean, nil] :lost_found (nil) write dangling objects to .git/lost-found
687
734
  # (note: this modifies the repository by creating files)
688
735
  # @option options [Boolean, nil] :dangling print dangling objects (true/false/nil for default)
689
736
  # @option options [Boolean, nil] :full check objects in alternate pools (true/false/nil for default)
@@ -694,10 +741,10 @@ module Git
694
741
  #
695
742
  # @example Check repository integrity
696
743
  # result = git.fsck
697
- # result.dangling.each { |obj| puts "#{obj.type}: #{obj.sha}" }
744
+ # result.dangling.each { |obj| puts "#{obj.type}: #{obj.oid}" }
698
745
  #
699
746
  # @example Check with strict mode and suppress dangling output
700
- # result = git.fsck(strict: true, dangling: false)
747
+ # result = git.fsck(strict: true, no_dangling: true)
701
748
  #
702
749
  # @example Check if repository has any issues
703
750
  # result = git.fsck
@@ -705,14 +752,14 @@ module Git
705
752
  #
706
753
  # @example List root commits
707
754
  # result = git.fsck(root: true)
708
- # result.root.each { |obj| puts obj.sha }
755
+ # result.root.each { |obj| puts obj.oid }
709
756
  #
710
757
  # @example Check specific objects
711
758
  # result = git.fsck('abc1234', 'def5678')
712
759
  #
713
760
  # rubocop:disable Style/ArgumentsForwarding
714
761
  def fsck(*objects, **opts)
715
- lib.fsck(*objects, **opts)
762
+ facade_repository.fsck(*objects, **opts)
716
763
  end
717
764
  # rubocop:enable Style/ArgumentsForwarding
718
765
 
@@ -732,7 +779,7 @@ module Git
732
779
  # @param [String|NilClass] path the path of the file to be shown
733
780
  # @return [String] the object information
734
781
  def show(objectish = nil, path = nil)
735
- lib.show(objectish, path)
782
+ facade_repository.show(objectish, path)
736
783
  end
737
784
 
738
785
  ## LOWER LEVEL INDEX OPERATIONS ##
@@ -761,7 +808,7 @@ module Git
761
808
  end
762
809
 
763
810
  def checkout_index(opts = {})
764
- lib.checkout_index(opts)
811
+ facade_repository.checkout_index(opts)
765
812
  end
766
813
 
767
814
  def read_tree(treeish, opts = {})
@@ -769,23 +816,22 @@ module Git
769
816
  end
770
817
 
771
818
  def write_tree
772
- lib.write_tree
819
+ facade_repository.write_tree
773
820
  end
774
821
 
775
822
  def write_and_commit_tree(opts = {})
776
- tree = write_tree
777
- commit_tree(tree, opts)
823
+ Git::Object::Commit.new(self, facade_repository.write_and_commit_tree(**opts))
778
824
  end
779
825
 
780
826
  def update_ref(branch, commit)
781
- branch(branch).update_ref(commit)
827
+ facade_repository.update_ref(branch, commit)
782
828
  end
783
829
 
784
830
  def ls_files(location = nil)
785
- lib.ls_files(location)
831
+ facade_repository.ls_files(location)
786
832
  end
787
833
 
788
- def with_working(work_dir) # :yields: the Git::WorkingDirectory
834
+ def with_working(work_dir) # :yields: the working directory Pathname
789
835
  return_value = false
790
836
  old_working = @working_directory
791
837
  set_working(work_dir)
@@ -813,29 +859,58 @@ module Git
813
859
  # git.rev_parse('v2.4:/doc/index.html')
814
860
  #
815
861
  def rev_parse(objectish)
816
- lib.rev_parse(objectish)
862
+ facade_repository.rev_parse(objectish)
817
863
  end
818
864
 
819
865
  # For backwards compatibility
820
866
  alias revparse rev_parse
821
867
 
822
- def ls_tree(objectish, opts = {})
823
- lib.ls_tree(objectish, opts)
868
+ # Returns the number of entries in a git tree object
869
+ #
870
+ # @example Count recursive entries in the HEAD tree
871
+ # git.tree_depth('HEAD^{tree}') #=> 42
872
+ #
873
+ # @param objectish [String] the tree-ish object to recurse into
874
+ #
875
+ # @return [Integer] the number of entries in the recursive tree listing
876
+ #
877
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
878
+ #
879
+ # @see Git::Repository::ObjectOperations#tree_depth
880
+ #
881
+ def tree_depth(objectish)
882
+ facade_repository.tree_depth(objectish)
824
883
  end
825
884
 
826
- # Returns the contents of a git object
885
+ # Lists the objects in a git tree object
827
886
  #
828
- # Uses `git cat-file -p` to pretty-print the contents of the given object.
887
+ # @example List all top-level objects
888
+ # git.ls_tree('HEAD')
889
+ # # => { 'blob' => { 'README.md' => { mode: '100644', sha: '...' } }, ... }
829
890
  #
830
- # @param objectish [String] a SHA, branch name, tag, or other revision reference
831
- # to the git object
891
+ # @param objectish [String] the tree-ish object to list
832
892
  #
833
- # @return [String] the contents of the object
893
+ # @param opts [Hash] additional options
834
894
  #
835
- # @see https://git-scm.com/docs/git-cat-file git-cat-file
895
+ # @option opts [Boolean, nil] :recursive (nil) recurse into subtrees
836
896
  #
897
+ # @option opts [String, Array<String>] :path (nil) limit the listing to
898
+ # the given path or array of paths
899
+ #
900
+ # @return [Hash<String, Hash<String, Hash>>] a three-level Hash keyed by
901
+ # object type (`'blob'`, `'tree'`, `'commit'`), then by filename, then
902
+ # holding `:mode` and `:sha` values
903
+ #
904
+ # @raise [ArgumentError] when unsupported options are provided
905
+ #
906
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
907
+ #
908
+ def ls_tree(objectish, opts = {})
909
+ facade_repository.ls_tree(objectish, opts)
910
+ end
911
+
837
912
  def cat_file(objectish)
838
- lib.cat_file_contents(objectish)
913
+ lib.cat_file(objectish)
839
914
  end
840
915
 
841
916
  # The name of the branch HEAD refers to or 'HEAD' if detached
@@ -847,59 +922,84 @@ module Git
847
922
  # @return [String] the name of the branch HEAD refers to or 'HEAD' if detached
848
923
  #
849
924
  def current_branch
850
- lib.branch_current
925
+ facade_repository.current_branch
851
926
  end
852
927
 
853
928
  # @return [Git::Branch] an object for branch_name
854
929
  def branch(branch_name = current_branch)
855
- Git::Branch.new(self, branch_name)
930
+ facade_repository.branch(branch_name)
856
931
  end
857
932
 
858
933
  # @return [Git::Branches] a collection of all the branches in the repository.
859
934
  # Each branch is represented as a {Git::Branch}.
860
935
  def branches
861
- Git::Branches.new(self)
936
+ facade_repository.branches
862
937
  end
863
938
 
864
- # returns a Git::Worktree object for dir, commitish
939
+ # Returns a {Git::Worktree} object for the given path and optional commitish
940
+ #
941
+ # @example Create a worktree object for an existing path
942
+ # worktree = repo.worktree('/path/to/worktree')
943
+ #
944
+ # @param dir [String] filesystem path of the worktree
945
+ #
946
+ # @param commitish [String, nil] branch, tag, or commit to check out
947
+ #
948
+ # @return [Git::Worktree] worktree object for the given path
949
+ #
865
950
  def worktree(dir, commitish = nil)
866
- Git::Worktree.new(self, dir, commitish)
951
+ facade_repository.worktree(dir, commitish)
867
952
  end
868
953
 
869
- # returns a Git::worktrees object of all the Git::Worktrees
870
- # objects for this repo
954
+ # Returns a {Git::Worktrees} collection of all worktrees in the repository
955
+ #
956
+ # @example List paths for all worktrees
957
+ # repo.worktrees.each { |wt| puts wt.dir }
958
+ #
959
+ # @return [Git::Worktrees] all linked and main worktrees
960
+ #
961
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
962
+ #
871
963
  def worktrees
872
- Git::Worktrees.new(self)
964
+ facade_repository.worktrees
873
965
  end
874
966
 
875
967
  # @return [Git::Object::Commit] a commit object
876
968
  def commit_tree(tree = nil, opts = {})
877
- Git::Object::Commit.new(self, lib.commit_tree(tree, opts))
969
+ Git::Object::Commit.new(self, facade_repository.commit_tree(tree, **opts))
878
970
  end
879
971
 
880
972
  # @return [Git::Diff] a Git::Diff object
881
973
  def diff(objectish = 'HEAD', obj2 = nil)
882
- Git::Diff.new(self, objectish, obj2)
974
+ facade_repository.diff(objectish, obj2)
883
975
  end
884
976
 
885
977
  # @return [Git::Object] a Git object
886
978
  def gblob(objectish)
887
- Git::Object.new(self, objectish, 'blob')
979
+ facade_repository.gblob(objectish)
888
980
  end
889
981
 
890
982
  # @return [Git::Object] a Git object
891
983
  def gcommit(objectish)
892
- Git::Object.new(self, objectish, 'commit')
984
+ facade_repository.gcommit(objectish)
893
985
  end
894
986
 
895
987
  # @return [Git::Object] a Git object
896
988
  def gtree(objectish)
897
- Git::Object.new(self, objectish, 'tree')
989
+ facade_repository.gtree(objectish)
898
990
  end
899
991
 
900
992
  # @return [Git::Log] a log with the specified number of commits
901
993
  def log(count = 30)
902
- Git::Log.new(self, count)
994
+ facade_repository.log(count)
995
+ end
996
+
997
+ # Return commits that are within the given revision range
998
+ #
999
+ # @param opts [Hash] options for the log query
1000
+ # @return [Array<Hash>] the parsed raw log output for each commit
1001
+ def full_log_commits(opts = {})
1002
+ facade_repository.full_log_commits(opts)
903
1003
  end
904
1004
 
905
1005
  # returns a Git::Object of the appropriate type
@@ -913,22 +1013,22 @@ module Git
913
1013
  #
914
1014
  # @return [Git::Object] an instance of the appropriate type of Git::Object
915
1015
  def object(objectish)
916
- Git::Object.new(self, objectish)
1016
+ facade_repository.object(objectish)
917
1017
  end
918
1018
 
919
1019
  # @return [Git::Remote] a remote of the specified name
920
1020
  def remote(remote_name = 'origin')
921
- Git::Remote.new(self, remote_name)
1021
+ facade_repository.remote(remote_name)
922
1022
  end
923
1023
 
924
1024
  # @return [Git::Status] a status object
925
1025
  def status
926
- Git::Status.new(self)
1026
+ facade_repository.status
927
1027
  end
928
1028
 
929
1029
  # @return [Git::Object::Tag] a tag object
930
1030
  def tag(tag_name)
931
- Git::Object::Tag.new(self, tag_name)
1031
+ facade_repository.tag(tag_name)
932
1032
  end
933
1033
 
934
1034
  # Find as good common ancestors as possible for a merge
@@ -936,204 +1036,169 @@ module Git
936
1036
  #
937
1037
  # @return [Array<Git::Object::Commit>] a collection of common ancestors
938
1038
  def merge_base(*)
939
- shas = lib.merge_base(*)
940
- shas.map { |sha| gcommit(sha) }
1039
+ facade_repository.merge_base(*).map { |sha| gcommit(sha) }
941
1040
  end
942
1041
 
943
- # Returns a Git::Diff::Stats object for accessing diff statistics.
1042
+ # Returns the full unified diff patch text between two commits
944
1043
  #
945
- # @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
946
- # @param obj2 [String, nil] The second commit or object to compare.
947
- # @param opts [Hash] Options to filter the diff.
948
- # @option opts [String, Pathname, Array<String, Pathname>] :path_limiter Limit stats to specified path(s).
949
- # @return [Git::DiffStats]
950
- def diff_stats(objectish = 'HEAD', obj2 = nil, opts = {})
951
- Git::DiffStats.new(self, objectish, obj2, opts[:path_limiter])
952
- end
953
-
954
- # Returns a Git::Diff::PathStatus object for accessing the name-status report.
1044
+ # @example Get the patch for the most recent commit
1045
+ # repo.diff_full #=> "diff --git a/lib/foo.rb b/lib/foo.rb\n..."
955
1046
  #
956
- # @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
957
- # @param obj2 [String, nil] The second commit or object to compare.
958
- # @param opts [Hash] Options to filter the diff.
959
- # @option opts [String, Pathname, Array<String, Pathname>] :path_limiter Limit status to specified path(s).
960
- # @option opts [String, Pathname, Array<String, Pathname>] :path (deprecated) Legacy alias for :path_limiter.
961
- # @return [Git::DiffPathStatus]
962
- def diff_path_status(objectish = 'HEAD', obj2 = nil, opts = {})
963
- path_limiter = if opts.key?(:path_limiter)
964
- opts[:path_limiter]
965
- elsif opts.key?(:path)
966
- Git::Deprecation.warn(
967
- 'Git::Base#diff_path_status :path option is deprecated. Use :path_limiter instead.'
968
- )
969
- opts[:path]
970
- end
971
-
972
- Git::DiffPathStatus.new(self, objectish, obj2, path_limiter)
973
- end
974
-
975
- # Provided for backwards compatibility
976
- alias diff_name_status diff_path_status
977
-
978
- private
979
-
980
- # Sets default paths in the options hash for direct `Git::Base.new` calls
1047
+ # @param obj1 [String] the first commit or object to compare; defaults to
1048
+ # `'HEAD'`
981
1049
  #
982
- # Factory methods like `Git.open` pre-populate these options by calling
983
- # `normalize_paths`, making this a fallback. It avoids mutating the
984
- # original options hash by returning a new one.
1050
+ # @param obj2 [String, nil] the second commit or object to compare
985
1051
  #
986
- # @param options [Hash] the original options hash
987
- # @return [Hash] a new options hash with defaults applied
988
- def default_paths(options)
989
- return options unless (working_dir = options[:working_directory])
990
-
991
- options.dup.tap do |opts|
992
- opts[:repository] ||= File.join(working_dir, '.git')
993
- opts[:index] ||= File.join(opts[:repository], 'index')
994
- end
995
- end
996
-
997
- # Initializes the logger from the provided options
998
- # @param log_option [Logger, nil] The logger instance from options.
999
- def setup_logger(log_option)
1000
- @logger = log_option || Logger.new(nil)
1001
- @logger.info('Starting Git')
1002
- end
1003
-
1004
- # Initializes the core git objects based on the provided options
1005
- # @param options [Hash] The processed options hash.
1006
- def initialize_components(options)
1007
- @working_directory = Git::WorkingDirectory.new(options[:working_directory]) if options[:working_directory]
1008
- @repository = Git::Repository.new(options[:repository]) if options[:repository]
1009
- @index = Git::Index.new(options[:index], must_exist: false) if options[:index]
1010
- end
1011
-
1012
- # Normalize options before they are sent to Git::Base.new
1052
+ # When `nil`, the comparison is against the index or working tree.
1013
1053
  #
1014
- # Updates the options parameter by setting appropriate values for the following keys:
1015
- # * options[:working_directory]
1016
- # * options[:repository]
1017
- # * options[:index]
1054
+ # @param opts [Hash] options to filter the diff
1018
1055
  #
1019
- # All three values will be set to absolute paths. An exception is that
1020
- # :working_directory will be set to nil if bare is true.
1056
+ # @option opts [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
1057
+ # limit the diff to the given path(s)
1021
1058
  #
1022
- private_class_method def self.normalize_paths(
1023
- options, default_working_directory: nil, default_repository: nil, bare: false
1024
- )
1025
- normalize_working_directory(options, default: default_working_directory, bare: bare)
1026
- normalize_repository(options, default: default_repository, bare: bare)
1027
- normalize_index(options)
1028
- end
1029
-
1030
- # Normalize options[:working_directory]
1059
+ # @return [String] the unified diff patch output
1031
1060
  #
1032
- # If working with a bare repository, set to `nil`.
1033
- # Otherwise, set to the first non-nil value of:
1034
- # 1. `options[:working_directory]`,
1035
- # 2. the `default` parameter, or
1036
- # 3. the current working directory
1061
+ # @note Unknown option keys are silently ignored for backward compatibility;
1062
+ # only `:path_limiter` is forwarded to the underlying command.
1037
1063
  #
1038
- # Finally, if options[:working_directory] is a relative path, convert it to an absoluite
1039
- # path relative to the current directory.
1064
+ # @raise [Git::FailedError] if git exits outside the allowed range (exit code > 1)
1040
1065
  #
1041
- private_class_method def self.normalize_working_directory(options, default:, bare: false)
1042
- working_directory =
1043
- if bare
1044
- nil
1045
- else
1046
- File.expand_path(options[:working_directory] || default || Dir.pwd)
1047
- end
1048
-
1049
- options[:working_directory] = working_directory
1066
+ # @see Git::Repository::Diffing#diff_full
1067
+ #
1068
+ def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
1069
+ facade_repository.diff_full(obj1, obj2, opts.slice(:path_limiter))
1050
1070
  end
1051
1071
 
1052
- # Normalize options[:repository]
1072
+ # Returns a lazy {Git::DiffStats} object for accessing diff statistics
1053
1073
  #
1054
- # If working with a bare repository, set to the first non-nil value out of:
1055
- # 1. `options[:repository]`
1056
- # 2. the `default` parameter
1057
- # 3. the current working directory
1074
+ # Compares (1) two commits, (2) a commit against the working tree, or (3) the
1075
+ # index against the working tree and constructs a lazy {Git::DiffStats} that
1076
+ # computes per-file insertion and deletion counts on demand when its accessor
1077
+ # methods are called.
1058
1078
  #
1059
- # Otherwise, set to the first non-nil value of:
1060
- # 1. `options[:repository]`
1061
- # 2. `.git`
1079
+ # **Comparing two commits**
1062
1080
  #
1063
- # Next, if options[:repository] refers to a *file* and not a *directory*, set
1064
- # options[:repository] to the contents of that file. This is the case when
1065
- # working with a submodule or a secondary working tree (created with git worktree
1066
- # add). In these cases the repository is actually contained/nested within the
1067
- # parent's repository directory.
1081
+ # When both objectish and obj2 are provided, the comparison is between those two
1082
+ # refs (commits, tags, branches, etc.).
1068
1083
  #
1069
- # Finally, if options[:repository] is a relative path, convert it to an absolute
1070
- # path relative to:
1071
- # 1. the current directory if working with a bare repository or
1072
- # 2. the working directory if NOT working with a bare repository
1084
+ # **Comparing a commit against the working tree**
1073
1085
  #
1074
- private_class_method def self.normalize_repository(options, default:, bare: false)
1075
- initial_path = initial_repository_path(options, default: default, bare: bare)
1076
- final_path = resolve_gitdir_if_present(initial_path, options[:working_directory])
1077
- options[:repository] = final_path
1078
- end
1079
-
1080
- # Determines the initial, potential path to the repository directory
1086
+ # When only objectish is provided (and isn't nil), the comparison is between
1087
+ # objectish and the working tree; the stats reflect all changes since objectish.
1081
1088
  #
1082
- # This path is considered 'initial' because it is not guaranteed to be the
1083
- # final repository location. For features like submodules or worktrees,
1084
- # this path may point to a text file containing a `gitdir:` pointer to the
1085
- # actual repository directory elsewhere. This initial path must be
1086
- # subsequently resolved.
1089
+ # **Comparing the index against the working tree**
1087
1090
  #
1088
- # @api private
1091
+ # When objectish is explicitly `nil` then obj2 must be omitted or `nil`. In this
1092
+ # case, the comparison is between the index and the working tree; the stats reflect
1093
+ # unstaged changes.
1089
1094
  #
1090
- # @param options [Hash] The options hash, checked for `[:repository]`.
1095
+ # @example Get working tree stats since HEAD
1096
+ # repo.diff_stats.insertions #=> 3
1091
1097
  #
1092
- # @param default [String] A fallback path if `options[:repository]` is not set.
1098
+ # @example Compare two specific commits
1099
+ # repo.diff_stats('abc1234', 'def5678')
1093
1100
  #
1094
- # @param bare [Boolean] Whether the repository is bare, which changes path resolution.
1101
+ # @example Get unstaged stats (index vs. working tree)
1102
+ # repo.diff_stats(nil).insertions
1095
1103
  #
1096
- # @return [String] The initial, absolute path to the `.git` directory or file.
1104
+ # @example Limit stats to a sub-path
1105
+ # repo.diff_stats('HEAD~1', 'HEAD', path_limiter: 'lib/')
1097
1106
  #
1098
- private_class_method def self.initial_repository_path(options, default:, bare:)
1099
- if bare
1100
- File.expand_path(options[:repository] || default || Dir.pwd)
1101
- else
1102
- File.expand_path(options[:repository] || '.git', options[:working_directory])
1103
- end
1107
+ # @param objectish [String, nil] the first commit or object to compare; defaults to
1108
+ # `'HEAD'`; pass `nil` to compare the index against the working tree
1109
+ #
1110
+ # @param obj2 [String, nil] the second commit or object to compare
1111
+ #
1112
+ # @param opts [Hash] options to filter the diff
1113
+ #
1114
+ # @option opts [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
1115
+ # limit the stats to the given path(s)
1116
+ #
1117
+ # @return [Git::DiffStats] a lazy stats object for the comparison
1118
+ #
1119
+ # @note Unknown option keys are silently ignored for backward compatibility;
1120
+ # only `:path_limiter` is forwarded to the underlying command.
1121
+ #
1122
+ # @raise [ArgumentError] if `objectish` or `obj2` starts with `"-"`
1123
+ #
1124
+ # @raise [ArgumentError] if `objectish` is `nil` but `obj2` is not
1125
+ #
1126
+ # @see Git::Repository::Diffing#diff_stats
1127
+ #
1128
+ def diff_stats(objectish = 'HEAD', obj2 = nil, opts = {})
1129
+ facade_repository.diff_stats(objectish, obj2, opts.slice(:path_limiter))
1104
1130
  end
1105
1131
 
1106
- # Resolves the path to the actual repository if it's a `gitdir:` pointer file.
1132
+ # Returns the file path status between two commits
1107
1133
  #
1108
- # If `path` points to a file (common in submodules and worktrees), this
1109
- # method reads the `gitdir:` path from it and returns the real repository
1110
- # path. Otherwise, it returns the original path.
1134
+ # @example Get all changed files between HEAD and the previous commit
1135
+ # repo.diff_path_status.to_h #=> { "README.md" => "M", "lib/foo.rb" => "A" }
1111
1136
  #
1112
- # @api private
1137
+ # @param objectish [String] the first commit or object to compare; defaults to
1138
+ # `'HEAD'`
1139
+ #
1140
+ # @param obj2 [String, nil] the second commit or object to compare
1113
1141
  #
1114
- # @param path [String] The initial path to the repository, which may be a pointer file.
1142
+ # @param opts [Hash] options to filter the diff
1115
1143
  #
1116
- # @param working_dir [String] The working directory, used as a base to resolve the path.
1144
+ # @option opts [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
1145
+ # limit the status report to specified path(s)
1117
1146
  #
1118
- # @return [String] The final, resolved absolute path to the repository directory.
1147
+ # @option opts [String, Pathname, Array<String, Pathname>, nil] :path (nil)
1148
+ # deprecated; use `:path_limiter` instead
1119
1149
  #
1120
- private_class_method def self.resolve_gitdir_if_present(path, working_dir)
1121
- return path unless File.file?(path)
1150
+ # @return [Git::DiffPathStatus] the name-status report for the comparison
1151
+ #
1152
+ # @raise [ArgumentError] if `objectish` or `obj2` starts with `"-"`
1153
+ #
1154
+ # @raise [Git::FailedError] if git exits outside the allowed range (exit code > 1)
1155
+ #
1156
+ # @see Git::Repository::Diffing#diff_path_status
1157
+ #
1158
+ def diff_path_status(objectish = 'HEAD', obj2 = nil, opts = {})
1159
+ facade_repository.diff_path_status(objectish, obj2, opts.slice(:path_limiter, :path))
1160
+ end
1122
1161
 
1123
- # The file contains `gitdir: <path>`, so we read the file,
1124
- # extract the path part, and expand it.
1125
- gitdir_pointer = File.read(path).sub(/\Agitdir: /, '').strip
1126
- File.expand_path(gitdir_pointer, working_dir)
1162
+ # Compares the index and the working directory
1163
+ #
1164
+ # @example List all files with unstaged changes
1165
+ # repo.diff_files #=> { "lib/foo.rb" => { mode_index: "100644", ... } }
1166
+ #
1167
+ # @return [Hash{String => Hash}] a hash keyed by file path; see
1168
+ # {Git::Repository::Diffing#diff_files} for the full key list
1169
+ #
1170
+ # @raise [Git::FailedError] if git exits outside the allowed range (exit code > 1)
1171
+ #
1172
+ # @see Git::Repository::Diffing#diff_files
1173
+ #
1174
+ def diff_files
1175
+ facade_repository.diff_files
1127
1176
  end
1128
1177
 
1129
- # Normalize options[:index]
1178
+ # Alias for {#diff_path_status}; provided for backward compatibility
1179
+ #
1180
+ # @return [Git::DiffPathStatus] the name-status report for the comparison
1130
1181
  #
1131
- # If options[:index] is a relative directory, convert it to an absolute
1132
- # directory relative to the repository directory
1182
+ # @deprecated Use {#diff_path_status} instead
1133
1183
  #
1134
- private_class_method def self.normalize_index(options)
1135
- index = File.expand_path(options[:index] || 'index', options[:repository])
1136
- options[:index] = index
1184
+ # @see #diff_path_status
1185
+ alias diff_name_status diff_path_status
1186
+
1187
+ private
1188
+
1189
+ # Initializes the logger from the provided options
1190
+ # @param log_option [Logger, nil] The logger instance from options.
1191
+ def setup_logger(log_option)
1192
+ @logger = log_option || Logger.new(nil)
1193
+ @logger.info('Starting Git')
1194
+ end
1195
+
1196
+ # Initializes the core git objects based on the provided options
1197
+ # @param options [Hash] The processed options hash.
1198
+ def initialize_components(options)
1199
+ @working_directory = Pathname.new(options[:working_directory]) if options[:working_directory]
1200
+ @repository = Pathname.new(options[:repository]) if options[:repository]
1201
+ @index = Pathname.new(options[:index]) if options[:index]
1137
1202
  end
1138
1203
  end
1139
1204
  end