git 4.4.3 → 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 (282) 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 -1
  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 -84
  45. data/CONTRIBUTING.md +694 -53
  46. data/LICENSE +1 -1
  47. data/README.md +17 -17
  48. data/Rakefile +61 -9
  49. data/commitlint.test +4 -0
  50. data/git.gemspec +15 -9
  51. data/lib/git/args_builder.rb +0 -8
  52. data/lib/git/base.rb +488 -424
  53. data/lib/git/branch.rb +117 -59
  54. data/lib/git/branch_delete_failure.rb +31 -0
  55. data/lib/git/branch_delete_result.rb +63 -0
  56. data/lib/git/branch_info.rb +178 -0
  57. data/lib/git/branches.rb +130 -24
  58. data/lib/git/command_line/base.rb +245 -0
  59. data/lib/git/command_line/capturing.rb +249 -0
  60. data/lib/git/command_line/result.rb +96 -0
  61. data/lib/git/command_line/streaming.rb +194 -0
  62. data/lib/git/command_line.rb +43 -322
  63. data/lib/git/command_line_result.rb +4 -88
  64. data/lib/git/commands/add.rb +131 -0
  65. data/lib/git/commands/am/abort.rb +43 -0
  66. data/lib/git/commands/am/apply.rb +252 -0
  67. data/lib/git/commands/am/continue.rb +43 -0
  68. data/lib/git/commands/am/quit.rb +43 -0
  69. data/lib/git/commands/am/retry.rb +47 -0
  70. data/lib/git/commands/am/show_current_patch.rb +64 -0
  71. data/lib/git/commands/am/skip.rb +42 -0
  72. data/lib/git/commands/am.rb +33 -0
  73. data/lib/git/commands/apply.rb +237 -0
  74. data/lib/git/commands/archive/list_formats.rb +46 -0
  75. data/lib/git/commands/archive.rb +140 -0
  76. data/lib/git/commands/arguments.rb +3510 -0
  77. data/lib/git/commands/base.rb +403 -0
  78. data/lib/git/commands/branch/copy.rb +94 -0
  79. data/lib/git/commands/branch/create.rb +173 -0
  80. data/lib/git/commands/branch/delete.rb +80 -0
  81. data/lib/git/commands/branch/list.rb +162 -0
  82. data/lib/git/commands/branch/move.rb +94 -0
  83. data/lib/git/commands/branch/set_upstream.rb +86 -0
  84. data/lib/git/commands/branch/show_current.rb +49 -0
  85. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  86. data/lib/git/commands/branch.rb +34 -0
  87. data/lib/git/commands/cat_file/batch.rb +364 -0
  88. data/lib/git/commands/cat_file/filtered.rb +105 -0
  89. data/lib/git/commands/cat_file/raw.rb +210 -0
  90. data/lib/git/commands/cat_file.rb +49 -0
  91. data/lib/git/commands/checkout/branch.rb +151 -0
  92. data/lib/git/commands/checkout/files.rb +115 -0
  93. data/lib/git/commands/checkout.rb +38 -0
  94. data/lib/git/commands/checkout_index.rb +105 -0
  95. data/lib/git/commands/clean.rb +100 -0
  96. data/lib/git/commands/clone.rb +240 -0
  97. data/lib/git/commands/commit.rb +272 -0
  98. data/lib/git/commands/commit_tree.rb +100 -0
  99. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  100. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  101. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  102. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  103. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  104. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  105. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
  106. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  107. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  108. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  109. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  110. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  111. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  112. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  113. data/lib/git/commands/config_option_syntax.rb +56 -0
  114. data/lib/git/commands/describe.rb +155 -0
  115. data/lib/git/commands/diff.rb +656 -0
  116. data/lib/git/commands/diff_files.rb +518 -0
  117. data/lib/git/commands/diff_index.rb +496 -0
  118. data/lib/git/commands/fetch.rb +352 -0
  119. data/lib/git/commands/fsck.rb +136 -0
  120. data/lib/git/commands/gc.rb +132 -0
  121. data/lib/git/commands/grep.rb +338 -0
  122. data/lib/git/commands/init.rb +99 -0
  123. data/lib/git/commands/log.rb +632 -0
  124. data/lib/git/commands/ls_files.rb +191 -0
  125. data/lib/git/commands/ls_remote.rb +155 -0
  126. data/lib/git/commands/ls_tree.rb +131 -0
  127. data/lib/git/commands/maintenance/register.rb +75 -0
  128. data/lib/git/commands/maintenance/run.rb +104 -0
  129. data/lib/git/commands/maintenance/start.rb +66 -0
  130. data/lib/git/commands/maintenance/stop.rb +55 -0
  131. data/lib/git/commands/maintenance/unregister.rb +79 -0
  132. data/lib/git/commands/maintenance.rb +31 -0
  133. data/lib/git/commands/merge/abort.rb +44 -0
  134. data/lib/git/commands/merge/continue.rb +44 -0
  135. data/lib/git/commands/merge/quit.rb +46 -0
  136. data/lib/git/commands/merge/start.rb +245 -0
  137. data/lib/git/commands/merge.rb +28 -0
  138. data/lib/git/commands/merge_base.rb +86 -0
  139. data/lib/git/commands/mv.rb +77 -0
  140. data/lib/git/commands/name_rev.rb +114 -0
  141. data/lib/git/commands/pull.rb +377 -0
  142. data/lib/git/commands/push.rb +246 -0
  143. data/lib/git/commands/read_tree.rb +149 -0
  144. data/lib/git/commands/remote/add.rb +91 -0
  145. data/lib/git/commands/remote/get_url.rb +66 -0
  146. data/lib/git/commands/remote/list.rb +54 -0
  147. data/lib/git/commands/remote/prune.rb +61 -0
  148. data/lib/git/commands/remote/remove.rb +52 -0
  149. data/lib/git/commands/remote/rename.rb +69 -0
  150. data/lib/git/commands/remote/set_branches.rb +63 -0
  151. data/lib/git/commands/remote/set_head.rb +82 -0
  152. data/lib/git/commands/remote/set_url.rb +71 -0
  153. data/lib/git/commands/remote/set_url_add.rb +61 -0
  154. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  155. data/lib/git/commands/remote/show.rb +71 -0
  156. data/lib/git/commands/remote/update.rb +72 -0
  157. data/lib/git/commands/remote.rb +42 -0
  158. data/lib/git/commands/repack.rb +277 -0
  159. data/lib/git/commands/reset.rb +147 -0
  160. data/lib/git/commands/rev_parse.rb +297 -0
  161. data/lib/git/commands/revert/abort.rb +45 -0
  162. data/lib/git/commands/revert/continue.rb +57 -0
  163. data/lib/git/commands/revert/quit.rb +47 -0
  164. data/lib/git/commands/revert/skip.rb +44 -0
  165. data/lib/git/commands/revert/start.rb +153 -0
  166. data/lib/git/commands/revert.rb +29 -0
  167. data/lib/git/commands/rm.rb +114 -0
  168. data/lib/git/commands/show.rb +632 -0
  169. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  170. data/lib/git/commands/show_ref/exists.rb +78 -0
  171. data/lib/git/commands/show_ref/list.rb +145 -0
  172. data/lib/git/commands/show_ref/verify.rb +120 -0
  173. data/lib/git/commands/show_ref.rb +42 -0
  174. data/lib/git/commands/stash/apply.rb +75 -0
  175. data/lib/git/commands/stash/branch.rb +65 -0
  176. data/lib/git/commands/stash/clear.rb +41 -0
  177. data/lib/git/commands/stash/create.rb +58 -0
  178. data/lib/git/commands/stash/drop.rb +67 -0
  179. data/lib/git/commands/stash/list.rb +39 -0
  180. data/lib/git/commands/stash/pop.rb +78 -0
  181. data/lib/git/commands/stash/push.rb +103 -0
  182. data/lib/git/commands/stash/show.rb +149 -0
  183. data/lib/git/commands/stash/store.rb +63 -0
  184. data/lib/git/commands/stash.rb +38 -0
  185. data/lib/git/commands/status.rb +169 -0
  186. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  187. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  188. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  189. data/lib/git/commands/symbolic_ref.rb +38 -0
  190. data/lib/git/commands/tag/create.rb +139 -0
  191. data/lib/git/commands/tag/delete.rb +55 -0
  192. data/lib/git/commands/tag/list.rb +143 -0
  193. data/lib/git/commands/tag/verify.rb +71 -0
  194. data/lib/git/commands/tag.rb +26 -0
  195. data/lib/git/commands/update_ref/batch.rb +140 -0
  196. data/lib/git/commands/update_ref/delete.rb +92 -0
  197. data/lib/git/commands/update_ref/update.rb +106 -0
  198. data/lib/git/commands/update_ref.rb +42 -0
  199. data/lib/git/commands/version.rb +52 -0
  200. data/lib/git/commands/worktree/add.rb +140 -0
  201. data/lib/git/commands/worktree/list.rb +64 -0
  202. data/lib/git/commands/worktree/lock.rb +58 -0
  203. data/lib/git/commands/worktree/management_base.rb +51 -0
  204. data/lib/git/commands/worktree/move.rb +66 -0
  205. data/lib/git/commands/worktree/prune.rb +67 -0
  206. data/lib/git/commands/worktree/remove.rb +63 -0
  207. data/lib/git/commands/worktree/repair.rb +76 -0
  208. data/lib/git/commands/worktree/unlock.rb +47 -0
  209. data/lib/git/commands/worktree.rb +43 -0
  210. data/lib/git/commands/write_tree.rb +68 -0
  211. data/lib/git/commands.rb +89 -0
  212. data/lib/git/detached_head_info.rb +54 -0
  213. data/lib/git/diff.rb +297 -7
  214. data/lib/git/diff_file_numstat_info.rb +29 -0
  215. data/lib/git/diff_file_patch_info.rb +134 -0
  216. data/lib/git/diff_file_raw_info.rb +127 -0
  217. data/lib/git/diff_info.rb +169 -0
  218. data/lib/git/diff_path_status.rb +78 -19
  219. data/lib/git/diff_result.rb +32 -0
  220. data/lib/git/diff_stats.rb +59 -14
  221. data/lib/git/dirstat_info.rb +86 -0
  222. data/lib/git/errors.rb +65 -2
  223. data/lib/git/execution_context/global.rb +56 -0
  224. data/lib/git/execution_context/repository.rb +147 -0
  225. data/lib/git/execution_context.rb +482 -0
  226. data/lib/git/file_ref.rb +74 -0
  227. data/lib/git/fsck_object.rb +9 -9
  228. data/lib/git/fsck_result.rb +1 -1
  229. data/lib/git/lib.rb +1606 -1084
  230. data/lib/git/log.rb +15 -2
  231. data/lib/git/object.rb +92 -22
  232. data/lib/git/parsers/branch.rb +224 -0
  233. data/lib/git/parsers/cat_file.rb +111 -0
  234. data/lib/git/parsers/diff.rb +585 -0
  235. data/lib/git/parsers/fsck.rb +133 -0
  236. data/lib/git/parsers/grep.rb +42 -0
  237. data/lib/git/parsers/ls_tree.rb +58 -0
  238. data/lib/git/parsers/stash.rb +208 -0
  239. data/lib/git/parsers/tag.rb +257 -0
  240. data/lib/git/remote.rb +133 -9
  241. data/lib/git/repository/branching.rb +572 -0
  242. data/lib/git/repository/committing.rb +191 -0
  243. data/lib/git/repository/configuring.rb +156 -0
  244. data/lib/git/repository/diffing.rb +775 -0
  245. data/lib/git/repository/inspecting.rb +153 -0
  246. data/lib/git/repository/logging.rb +247 -0
  247. data/lib/git/repository/merging.rb +295 -0
  248. data/lib/git/repository/object_operations.rb +1101 -0
  249. data/lib/git/repository/path_resolver.rb +207 -0
  250. data/lib/git/repository/remote_operations.rb +753 -0
  251. data/lib/git/repository/shared_private.rb +51 -0
  252. data/lib/git/repository/staging.rb +390 -0
  253. data/lib/git/repository/stashing.rb +107 -0
  254. data/lib/git/repository/status_operations.rb +180 -0
  255. data/lib/git/repository/worktree_operations.rb +159 -0
  256. data/lib/git/repository.rb +264 -1
  257. data/lib/git/stash.rb +85 -4
  258. data/lib/git/stash_info.rb +104 -0
  259. data/lib/git/stashes.rb +130 -13
  260. data/lib/git/status.rb +226 -18
  261. data/lib/git/tag_delete_failure.rb +31 -0
  262. data/lib/git/tag_delete_result.rb +63 -0
  263. data/lib/git/tag_info.rb +105 -0
  264. data/lib/git/version.rb +109 -2
  265. data/lib/git/version_constraint.rb +81 -0
  266. data/lib/git/worktree.rb +120 -5
  267. data/lib/git/worktrees.rb +107 -7
  268. data/lib/git.rb +117 -56
  269. data/redesign/1_architecture_existing.md +54 -18
  270. data/redesign/2_architecture_redesign.md +365 -46
  271. data/redesign/3_architecture_implementation.md +1451 -54
  272. data/tasks/gem_tasks.rake +4 -0
  273. data/tasks/npm_tasks.rake +7 -0
  274. data/tasks/rspec.rake +48 -0
  275. data/tasks/rubocop.rake +2 -9
  276. data/tasks/test.rake +13 -1
  277. data/tasks/yard.rake +34 -7
  278. metadata +351 -22
  279. data/lib/git/index.rb +0 -6
  280. data/lib/git/path.rb +0 -38
  281. data/lib/git/working_directory.rb +0 -6
  282. /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,43 +268,46 @@ 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
- # @deprecated Use {#local_branch?} instead
310
311
  def is_local_branch?(branch) # rubocop:disable Naming/PredicatePrefix
311
312
  Git::Deprecation.warn(
312
313
  'Git::Base#is_local_branch? is deprecated and will be removed in a future version. ' \
@@ -317,11 +318,9 @@ module Git
317
318
 
318
319
  # returns +true+ if the branch exists remotely
319
320
  def remote_branch?(branch)
320
- branch_names = branches.remote.map(&:name)
321
- branch_names.include?(branch)
321
+ facade_repository.remote_branch?(branch)
322
322
  end
323
323
 
324
- # @deprecated Use {#remote_branch?} instead
325
324
  def is_remote_branch?(branch) # rubocop:disable Naming/PredicatePrefix
326
325
  Git::Deprecation.warn(
327
326
  'Git::Base#is_remote_branch? is deprecated and will be removed in a future version. ' \
@@ -332,11 +331,9 @@ module Git
332
331
 
333
332
  # returns +true+ if the branch exists
334
333
  def branch?(branch)
335
- branch_names = branches.map(&:name)
336
- branch_names.include?(branch)
334
+ facade_repository.branch?(branch)
337
335
  end
338
336
 
339
- # @deprecated Use {#branch?} instead
340
337
  def is_branch?(branch) # rubocop:disable Naming/PredicatePrefix
341
338
  Git::Deprecation.warn(
342
339
  'Git::Base#is_branch? is deprecated and will be removed in a future version. ' \
@@ -349,10 +346,20 @@ module Git
349
346
  # actual 'git' forked system calls. At some point I hope to replace the Git::Lib
350
347
  # class with one that uses native methods or libgit C bindings
351
348
  def lib
352
- @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
+ )
353
360
  end
354
361
 
355
- # Returns the per-instance git_ssh configuration value.
362
+ # Returns the per-instance git_ssh configuration value
356
363
  #
357
364
  # This may be:
358
365
  # * a [String] path when an explicit git_ssh command has been configured
@@ -363,6 +370,16 @@ module Git
363
370
  # @api private
364
371
  attr_reader :git_ssh
365
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
+
366
383
  # Run a grep for 'string' on the HEAD of the git repository
367
384
  #
368
385
  # @example Limit grep's scope by calling grep() from a specific object:
@@ -381,9 +398,9 @@ module Git
381
398
  # of paths to limit the search to or nil for no limit
382
399
  # @param opts [Hash] options to pass to the underlying `git grep` command
383
400
  #
384
- # @option opts [Boolean] :ignore_case (false) ignore case when matching
385
- # @option opts [Boolean] :invert_match (false) select non-matching lines
386
- # @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
387
404
  # @option opts [String] :object (HEAD) the object to search from
388
405
  #
389
406
  # @return [Hash<String, Array>] a hash of arrays
@@ -396,30 +413,38 @@ module Git
396
413
  # ```
397
414
  #
398
415
  def grep(string, path_limiter = nil, opts = {})
399
- 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)
400
418
  end
401
419
 
402
420
  # List the files in the worktree that are ignored by git
403
- # @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
404
422
  #
405
423
  def ignored_files
406
- lib.ignored_files
424
+ facade_repository.ignored_files
407
425
  end
408
426
 
409
427
  # removes file(s) from the git repository
410
428
  def rm(path = '.', opts = {})
411
- lib.rm(path, opts)
429
+ facade_repository.rm(path, opts)
412
430
  end
413
431
 
414
432
  alias remove rm
415
433
 
416
434
  # resets the working directory to the provided commitish
417
435
  def reset(commitish = nil, opts = {})
418
- lib.reset(commitish, opts)
436
+ facade_repository.reset(commitish, **opts)
419
437
  end
420
438
 
421
439
  # resets the working directory to the commitish with '--hard'
440
+ #
441
+ # @deprecated Use {#reset} with `hard: true` instead.
442
+ #
422
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
+ )
423
448
  opts = { hard: true }.merge(opts)
424
449
  lib.reset(commitish, opts)
425
450
  end
@@ -432,7 +457,7 @@ module Git
432
457
  # :ff
433
458
  #
434
459
  def clean(opts = {})
435
- lib.clean(opts)
460
+ facade_repository.clean(opts)
436
461
  end
437
462
 
438
463
  # returns the most recent tag that is reachable from a commit
@@ -461,7 +486,7 @@ module Git
461
486
  # :no_edit
462
487
  #
463
488
  def revert(commitish = nil, opts = {})
464
- lib.revert(commitish, opts)
489
+ facade_repository.revert(commitish, **opts)
465
490
  end
466
491
 
467
492
  # commits all pending changes in the index file to the git repository
@@ -473,69 +498,71 @@ module Git
473
498
  # :author
474
499
  #
475
500
  def commit(message, opts = {})
476
- lib.commit(message, opts)
501
+ facade_repository.commit(message, **opts)
477
502
  end
478
503
 
479
504
  # commits all pending changes in the index file to the git repository,
480
505
  # but automatically adds all modified files without having to explicitly
481
506
  # calling @git.add() on them.
482
507
  def commit_all(message, opts = {})
483
- opts = { add_all: true }.merge(opts)
484
- lib.commit(message, opts)
508
+ facade_repository.commit_all(message, **opts)
485
509
  end
486
510
 
487
511
  # checks out a branch as the new git working directory
488
512
  def checkout(*, **)
489
- lib.checkout(*, **)
513
+ facade_repository.checkout(*, **)
490
514
  end
491
515
 
492
516
  # checks out an old version of a file
493
517
  def checkout_file(version, file)
494
- lib.checkout_file(version, file)
518
+ facade_repository.checkout_file(version, file)
495
519
  end
496
520
 
497
521
  # fetches changes from a remote branch - this does not modify the working directory,
498
522
  # it just gets the changes from the remote if there are any
499
523
  def fetch(remote = 'origin', opts = {})
500
- if remote.is_a?(Hash)
501
- opts = remote
502
- remote = nil
503
- end
504
- lib.fetch(remote, opts)
524
+ facade_repository.fetch(remote, opts)
505
525
  end
506
526
 
507
527
  # Push changes to a remote repository
508
528
  #
509
529
  # @overload push(remote = nil, branch = nil, options = {})
530
+ #
510
531
  # @param remote [String] the remote repository to push to
532
+ #
511
533
  # @param branch [String] the branch to push
534
+ #
512
535
  # @param options [Hash] options to pass to the push command
513
536
  #
514
- # @option opts [Boolean] :mirror (false) Push all refs under refs/heads/, refs/tags/ and refs/remotes/
515
- # @option opts [Boolean] :delete (false) Delete refs that don't exist on the remote
516
- # @option opts [Boolean] :force (false) Force updates
517
- # @option opts [Boolean] :tags (false) Push all refs under refs/tags/
518
- # @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/
519
544
  #
520
- # @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
521
548
  #
522
549
  # @raise [Git::FailedError] if the push fails
523
550
  # @raise [ArgumentError] if a branch is given without a remote
524
551
  #
525
552
  def push(*, **)
526
- lib.push(*, **)
553
+ facade_repository.push(*, **)
527
554
  end
528
555
 
529
556
  # merges one or more branches into the current working branch
530
557
  #
531
558
  # you can specify more than one branch to merge by passing an array of branches
532
559
  def merge(branch, message = 'merge', opts = {})
533
- lib.merge(branch, message, opts)
560
+ facade_repository.merge(branch, message, opts)
534
561
  end
535
562
 
536
563
  # iterates over the files which are unmerged
537
- def each_conflict(&) # :yields: file, your_version, their_version
538
- lib.conflicts(&)
564
+ def each_conflict(&)
565
+ facade_repository.each_conflict(&)
539
566
  end
540
567
 
541
568
  # Pulls the given branch from the given remote into the current branch
@@ -544,7 +571,7 @@ module Git
544
571
  # @param branch [String] the branch to pull from
545
572
  # @param opts [Hash] options to pass to the pull command
546
573
  #
547
- # @option opts [Boolean] :allow_unrelated_histories (false) Merges histories of
574
+ # @option opts [Boolean, nil] :allow_unrelated_histories (nil) merges histories of
548
575
  # two projects that started their lives independently
549
576
  # @example pulls from origin/master
550
577
  # @git.pull
@@ -553,17 +580,17 @@ module Git
553
580
  # @example pulls from upstream/develop
554
581
  # @git.pull('upstream', 'develop')
555
582
  #
556
- # @return [Void]
583
+ # @return [String] the stdout output from the pull command
557
584
  #
558
585
  # @raise [Git::FailedError] if the pull fails
559
586
  # @raise [ArgumentError] if a branch is given without a remote
560
587
  def pull(remote = nil, branch = nil, opts = {})
561
- lib.pull(remote, branch, opts)
588
+ facade_repository.pull(remote, branch, opts)
562
589
  end
563
590
 
564
591
  # returns an array of Git:Remote objects
565
592
  def remotes
566
- lib.remotes.map { |r| Git::Remote.new(self, r) }
593
+ facade_repository.remotes
567
594
  end
568
595
 
569
596
  # sets the url for a remote
@@ -572,9 +599,7 @@ module Git
572
599
  # @git.set_remote_url('scotts_git', 'git://repo.or.cz/rubygit.git')
573
600
  #
574
601
  def set_remote_url(name, url)
575
- url = url.repo.to_s if url.is_a?(Git::Base)
576
- lib.remote_set_url(name, url)
577
- Git::Remote.new(self, name)
602
+ facade_repository.set_remote_url(name, url)
578
603
  end
579
604
 
580
605
  # Configures which branches are fetched for a remote
@@ -606,24 +631,19 @@ module Git
606
631
  # the underlying git command fails
607
632
  #
608
633
  def remote_set_branches(name, *branches, add: false)
609
- branch_list = branches.flatten
610
- raise ArgumentError, 'branches are required' if branch_list.empty?
611
-
612
- lib.remote_set_branches(name, branch_list, add: add)
613
-
614
- nil
634
+ facade_repository.remote_set_branches(name, *branches, add: add)
615
635
  end
616
636
 
617
637
  # removes a remote from this repository
618
638
  #
619
639
  # @git.remove_remote('scott_git')
620
640
  def remove_remote(name)
621
- lib.remote_remove(name)
641
+ facade_repository.remove_remote(name)
622
642
  end
623
643
 
624
- # returns an array of all Git::Tag objects for this repository
644
+ # returns an array of all Git::Object::Tag objects for this repository
625
645
  def tags
626
- lib.tags.map { |r| tag(r) }
646
+ facade_repository.tags
627
647
  end
628
648
 
629
649
  # Create a new git tag
@@ -634,29 +654,53 @@ module Git
634
654
  # repo.add_tag('tag_name', {:options => 'here'})
635
655
  #
636
656
  # @param [String] name The name of the tag to add
637
- # @param [Hash] options Opstions to pass to `git tag`.
657
+ # @param [Hash] options Options to pass to `git tag`.
638
658
  # See [git-tag](https://git-scm.com/docs/git-tag) for more details.
639
- # @option options [boolean] :annotate Make an unsigned, annotated tag object
640
- # @option options [boolean] :a An alias for the `:annotate` option
641
- # @option options [boolean] :d Delete existing tag with the given names.
642
- # @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)
643
666
  # @option options [String] :message Use the given tag message
644
667
  # @option options [String] :m An alias for the `:message` option
645
- # @option options [boolean] :s Make a GPG-signed tag.
668
+ # @option options [Boolean, nil] :s (nil) make a GPG-signed tag
646
669
  #
647
670
  def add_tag(name, *options)
648
- lib.tag(name, *options)
649
- tag(name)
671
+ facade_repository.add_tag(name, *options)
650
672
  end
651
673
 
652
674
  # deletes a tag
653
675
  def delete_tag(name)
654
- lib.tag(name, { d: true })
676
+ facade_repository.delete_tag(name)
655
677
  end
656
678
 
657
- # 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
+ #
658
702
  def archive(treeish, file = nil, opts = {})
659
- object(treeish).archive(file, opts)
703
+ facade_repository.archive(treeish, file, opts)
660
704
  end
661
705
 
662
706
  # repacks the repository
@@ -679,14 +723,14 @@ module Git
679
723
  # references in the refs namespace, and all reflogs.
680
724
  # @param [Hash] options options to pass to the underlying `git fsck` command
681
725
  #
682
- # @option options [Boolean] :unreachable print unreachable objects
683
- # @option options [Boolean] :strict enable strict checking
684
- # @option options [Boolean] :connectivity_only check only connectivity (faster)
685
- # @option options [Boolean] :root report root nodes
686
- # @option options [Boolean] :tags report tags
687
- # @option options [Boolean] :cache consider objects in the index
688
- # @option options [Boolean] :no_reflogs do not consider reflogs
689
- # @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
690
734
  # (note: this modifies the repository by creating files)
691
735
  # @option options [Boolean, nil] :dangling print dangling objects (true/false/nil for default)
692
736
  # @option options [Boolean, nil] :full check objects in alternate pools (true/false/nil for default)
@@ -697,10 +741,10 @@ module Git
697
741
  #
698
742
  # @example Check repository integrity
699
743
  # result = git.fsck
700
- # result.dangling.each { |obj| puts "#{obj.type}: #{obj.sha}" }
744
+ # result.dangling.each { |obj| puts "#{obj.type}: #{obj.oid}" }
701
745
  #
702
746
  # @example Check with strict mode and suppress dangling output
703
- # result = git.fsck(strict: true, dangling: false)
747
+ # result = git.fsck(strict: true, no_dangling: true)
704
748
  #
705
749
  # @example Check if repository has any issues
706
750
  # result = git.fsck
@@ -708,14 +752,16 @@ module Git
708
752
  #
709
753
  # @example List root commits
710
754
  # result = git.fsck(root: true)
711
- # result.root.each { |obj| puts obj.sha }
755
+ # result.root.each { |obj| puts obj.oid }
712
756
  #
713
757
  # @example Check specific objects
714
758
  # result = git.fsck('abc1234', 'def5678')
715
759
  #
716
- def fsck(*objects, **opts) # rubocop:disable Style/ArgumentsForwarding
717
- lib.fsck(*objects, **opts) # rubocop:disable Style/ArgumentsForwarding
760
+ # rubocop:disable Style/ArgumentsForwarding
761
+ def fsck(*objects, **opts)
762
+ facade_repository.fsck(*objects, **opts)
718
763
  end
764
+ # rubocop:enable Style/ArgumentsForwarding
719
765
 
720
766
  def apply(file)
721
767
  return unless File.exist?(file)
@@ -733,7 +779,7 @@ module Git
733
779
  # @param [String|NilClass] path the path of the file to be shown
734
780
  # @return [String] the object information
735
781
  def show(objectish = nil, path = nil)
736
- lib.show(objectish, path)
782
+ facade_repository.show(objectish, path)
737
783
  end
738
784
 
739
785
  ## LOWER LEVEL INDEX OPERATIONS ##
@@ -762,7 +808,7 @@ module Git
762
808
  end
763
809
 
764
810
  def checkout_index(opts = {})
765
- lib.checkout_index(opts)
811
+ facade_repository.checkout_index(opts)
766
812
  end
767
813
 
768
814
  def read_tree(treeish, opts = {})
@@ -770,23 +816,22 @@ module Git
770
816
  end
771
817
 
772
818
  def write_tree
773
- lib.write_tree
819
+ facade_repository.write_tree
774
820
  end
775
821
 
776
822
  def write_and_commit_tree(opts = {})
777
- tree = write_tree
778
- commit_tree(tree, opts)
823
+ Git::Object::Commit.new(self, facade_repository.write_and_commit_tree(**opts))
779
824
  end
780
825
 
781
826
  def update_ref(branch, commit)
782
- branch(branch).update_ref(commit)
827
+ facade_repository.update_ref(branch, commit)
783
828
  end
784
829
 
785
830
  def ls_files(location = nil)
786
- lib.ls_files(location)
831
+ facade_repository.ls_files(location)
787
832
  end
788
833
 
789
- def with_working(work_dir) # :yields: the Git::WorkingDirectory
834
+ def with_working(work_dir) # :yields: the working directory Pathname
790
835
  return_value = false
791
836
  old_working = @working_directory
792
837
  set_working(work_dir)
@@ -814,29 +859,58 @@ module Git
814
859
  # git.rev_parse('v2.4:/doc/index.html')
815
860
  #
816
861
  def rev_parse(objectish)
817
- lib.rev_parse(objectish)
862
+ facade_repository.rev_parse(objectish)
818
863
  end
819
864
 
820
865
  # For backwards compatibility
821
866
  alias revparse rev_parse
822
867
 
823
- def ls_tree(objectish, opts = {})
824
- 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)
825
883
  end
826
884
 
827
- # Returns the contents of a git object
885
+ # Lists the objects in a git tree object
828
886
  #
829
- # 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: '...' } }, ... }
830
890
  #
831
- # @param objectish [String] a SHA, branch name, tag, or other revision reference
832
- # to the git object
891
+ # @param objectish [String] the tree-ish object to list
833
892
  #
834
- # @return [String] the contents of the object
893
+ # @param opts [Hash] additional options
835
894
  #
836
- # @see https://git-scm.com/docs/git-cat-file git-cat-file
895
+ # @option opts [Boolean, nil] :recursive (nil) recurse into subtrees
837
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
+
838
912
  def cat_file(objectish)
839
- lib.cat_file_contents(objectish)
913
+ lib.cat_file(objectish)
840
914
  end
841
915
 
842
916
  # The name of the branch HEAD refers to or 'HEAD' if detached
@@ -848,59 +922,84 @@ module Git
848
922
  # @return [String] the name of the branch HEAD refers to or 'HEAD' if detached
849
923
  #
850
924
  def current_branch
851
- lib.branch_current
925
+ facade_repository.current_branch
852
926
  end
853
927
 
854
928
  # @return [Git::Branch] an object for branch_name
855
929
  def branch(branch_name = current_branch)
856
- Git::Branch.new(self, branch_name)
930
+ facade_repository.branch(branch_name)
857
931
  end
858
932
 
859
933
  # @return [Git::Branches] a collection of all the branches in the repository.
860
934
  # Each branch is represented as a {Git::Branch}.
861
935
  def branches
862
- Git::Branches.new(self)
936
+ facade_repository.branches
863
937
  end
864
938
 
865
- # 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
+ #
866
950
  def worktree(dir, commitish = nil)
867
- Git::Worktree.new(self, dir, commitish)
951
+ facade_repository.worktree(dir, commitish)
868
952
  end
869
953
 
870
- # returns a Git::worktrees object of all the Git::Worktrees
871
- # 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
+ #
872
963
  def worktrees
873
- Git::Worktrees.new(self)
964
+ facade_repository.worktrees
874
965
  end
875
966
 
876
967
  # @return [Git::Object::Commit] a commit object
877
968
  def commit_tree(tree = nil, opts = {})
878
- Git::Object::Commit.new(self, lib.commit_tree(tree, opts))
969
+ Git::Object::Commit.new(self, facade_repository.commit_tree(tree, **opts))
879
970
  end
880
971
 
881
972
  # @return [Git::Diff] a Git::Diff object
882
973
  def diff(objectish = 'HEAD', obj2 = nil)
883
- Git::Diff.new(self, objectish, obj2)
974
+ facade_repository.diff(objectish, obj2)
884
975
  end
885
976
 
886
977
  # @return [Git::Object] a Git object
887
978
  def gblob(objectish)
888
- Git::Object.new(self, objectish, 'blob')
979
+ facade_repository.gblob(objectish)
889
980
  end
890
981
 
891
982
  # @return [Git::Object] a Git object
892
983
  def gcommit(objectish)
893
- Git::Object.new(self, objectish, 'commit')
984
+ facade_repository.gcommit(objectish)
894
985
  end
895
986
 
896
987
  # @return [Git::Object] a Git object
897
988
  def gtree(objectish)
898
- Git::Object.new(self, objectish, 'tree')
989
+ facade_repository.gtree(objectish)
899
990
  end
900
991
 
901
992
  # @return [Git::Log] a log with the specified number of commits
902
993
  def log(count = 30)
903
- 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)
904
1003
  end
905
1004
 
906
1005
  # returns a Git::Object of the appropriate type
@@ -914,22 +1013,22 @@ module Git
914
1013
  #
915
1014
  # @return [Git::Object] an instance of the appropriate type of Git::Object
916
1015
  def object(objectish)
917
- Git::Object.new(self, objectish)
1016
+ facade_repository.object(objectish)
918
1017
  end
919
1018
 
920
1019
  # @return [Git::Remote] a remote of the specified name
921
1020
  def remote(remote_name = 'origin')
922
- Git::Remote.new(self, remote_name)
1021
+ facade_repository.remote(remote_name)
923
1022
  end
924
1023
 
925
1024
  # @return [Git::Status] a status object
926
1025
  def status
927
- Git::Status.new(self)
1026
+ facade_repository.status
928
1027
  end
929
1028
 
930
1029
  # @return [Git::Object::Tag] a tag object
931
1030
  def tag(tag_name)
932
- Git::Object::Tag.new(self, tag_name)
1031
+ facade_repository.tag(tag_name)
933
1032
  end
934
1033
 
935
1034
  # Find as good common ancestors as possible for a merge
@@ -937,204 +1036,169 @@ module Git
937
1036
  #
938
1037
  # @return [Array<Git::Object::Commit>] a collection of common ancestors
939
1038
  def merge_base(*)
940
- shas = lib.merge_base(*)
941
- shas.map { |sha| gcommit(sha) }
1039
+ facade_repository.merge_base(*).map { |sha| gcommit(sha) }
942
1040
  end
943
1041
 
944
- # Returns a Git::Diff::Stats object for accessing diff statistics.
1042
+ # Returns the full unified diff patch text between two commits
945
1043
  #
946
- # @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
947
- # @param obj2 [String, nil] The second commit or object to compare.
948
- # @param opts [Hash] Options to filter the diff.
949
- # @option opts [String, Pathname, Array<String, Pathname>] :path_limiter Limit stats to specified path(s).
950
- # @return [Git::DiffStats]
951
- def diff_stats(objectish = 'HEAD', obj2 = nil, opts = {})
952
- Git::DiffStats.new(self, objectish, obj2, opts[:path_limiter])
953
- end
954
-
955
- # 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..."
956
1046
  #
957
- # @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
958
- # @param obj2 [String, nil] The second commit or object to compare.
959
- # @param opts [Hash] Options to filter the diff.
960
- # @option opts [String, Pathname, Array<String, Pathname>] :path_limiter Limit status to specified path(s).
961
- # @option opts [String, Pathname, Array<String, Pathname>] :path (deprecated) Legacy alias for :path_limiter.
962
- # @return [Git::DiffPathStatus]
963
- def diff_path_status(objectish = 'HEAD', obj2 = nil, opts = {})
964
- path_limiter = if opts.key?(:path_limiter)
965
- opts[:path_limiter]
966
- elsif opts.key?(:path)
967
- Git::Deprecation.warn(
968
- 'Git::Base#diff_path_status :path option is deprecated. Use :path_limiter instead.'
969
- )
970
- opts[:path]
971
- end
972
-
973
- Git::DiffPathStatus.new(self, objectish, obj2, path_limiter)
974
- end
975
-
976
- # Provided for backwards compatibility
977
- alias diff_name_status diff_path_status
978
-
979
- private
980
-
981
- # 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'`
982
1049
  #
983
- # Factory methods like `Git.open` pre-populate these options by calling
984
- # `normalize_paths`, making this a fallback. It avoids mutating the
985
- # original options hash by returning a new one.
1050
+ # @param obj2 [String, nil] the second commit or object to compare
986
1051
  #
987
- # @param options [Hash] the original options hash
988
- # @return [Hash] a new options hash with defaults applied
989
- def default_paths(options)
990
- return options unless (working_dir = options[:working_directory])
991
-
992
- options.dup.tap do |opts|
993
- opts[:repository] ||= File.join(working_dir, '.git')
994
- opts[:index] ||= File.join(opts[:repository], 'index')
995
- end
996
- end
997
-
998
- # Initializes the logger from the provided options
999
- # @param log_option [Logger, nil] The logger instance from options.
1000
- def setup_logger(log_option)
1001
- @logger = log_option || Logger.new(nil)
1002
- @logger.info('Starting Git')
1003
- end
1004
-
1005
- # Initializes the core git objects based on the provided options
1006
- # @param options [Hash] The processed options hash.
1007
- def initialize_components(options)
1008
- @working_directory = Git::WorkingDirectory.new(options[:working_directory]) if options[:working_directory]
1009
- @repository = Git::Repository.new(options[:repository]) if options[:repository]
1010
- @index = Git::Index.new(options[:index], must_exist: false) if options[:index]
1011
- end
1012
-
1013
- # Normalize options before they are sent to Git::Base.new
1052
+ # When `nil`, the comparison is against the index or working tree.
1014
1053
  #
1015
- # Updates the options parameter by setting appropriate values for the following keys:
1016
- # * options[:working_directory]
1017
- # * options[:repository]
1018
- # * options[:index]
1054
+ # @param opts [Hash] options to filter the diff
1019
1055
  #
1020
- # All three values will be set to absolute paths. An exception is that
1021
- # :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)
1022
1058
  #
1023
- private_class_method def self.normalize_paths(
1024
- options, default_working_directory: nil, default_repository: nil, bare: false
1025
- )
1026
- normalize_working_directory(options, default: default_working_directory, bare: bare)
1027
- normalize_repository(options, default: default_repository, bare: bare)
1028
- normalize_index(options)
1029
- end
1030
-
1031
- # Normalize options[:working_directory]
1059
+ # @return [String] the unified diff patch output
1032
1060
  #
1033
- # If working with a bare repository, set to `nil`.
1034
- # Otherwise, set to the first non-nil value of:
1035
- # 1. `options[:working_directory]`,
1036
- # 2. the `default` parameter, or
1037
- # 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.
1038
1063
  #
1039
- # Finally, if options[:working_directory] is a relative path, convert it to an absoluite
1040
- # path relative to the current directory.
1064
+ # @raise [Git::FailedError] if git exits outside the allowed range (exit code > 1)
1041
1065
  #
1042
- private_class_method def self.normalize_working_directory(options, default:, bare: false)
1043
- working_directory =
1044
- if bare
1045
- nil
1046
- else
1047
- File.expand_path(options[:working_directory] || default || Dir.pwd)
1048
- end
1049
-
1050
- 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))
1051
1070
  end
1052
1071
 
1053
- # Normalize options[:repository]
1072
+ # Returns a lazy {Git::DiffStats} object for accessing diff statistics
1054
1073
  #
1055
- # If working with a bare repository, set to the first non-nil value out of:
1056
- # 1. `options[:repository]`
1057
- # 2. the `default` parameter
1058
- # 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.
1059
1078
  #
1060
- # Otherwise, set to the first non-nil value of:
1061
- # 1. `options[:repository]`
1062
- # 2. `.git`
1079
+ # **Comparing two commits**
1063
1080
  #
1064
- # Next, if options[:repository] refers to a *file* and not a *directory*, set
1065
- # options[:repository] to the contents of that file. This is the case when
1066
- # working with a submodule or a secondary working tree (created with git worktree
1067
- # add). In these cases the repository is actually contained/nested within the
1068
- # parent's repository directory.
1081
+ # When both objectish and obj2 are provided, the comparison is between those two
1082
+ # refs (commits, tags, branches, etc.).
1069
1083
  #
1070
- # Finally, if options[:repository] is a relative path, convert it to an absolute
1071
- # path relative to:
1072
- # 1. the current directory if working with a bare repository or
1073
- # 2. the working directory if NOT working with a bare repository
1084
+ # **Comparing a commit against the working tree**
1074
1085
  #
1075
- private_class_method def self.normalize_repository(options, default:, bare: false)
1076
- initial_path = initial_repository_path(options, default: default, bare: bare)
1077
- final_path = resolve_gitdir_if_present(initial_path, options[:working_directory])
1078
- options[:repository] = final_path
1079
- end
1080
-
1081
- # 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.
1082
1088
  #
1083
- # This path is considered 'initial' because it is not guaranteed to be the
1084
- # final repository location. For features like submodules or worktrees,
1085
- # this path may point to a text file containing a `gitdir:` pointer to the
1086
- # actual repository directory elsewhere. This initial path must be
1087
- # subsequently resolved.
1089
+ # **Comparing the index against the working tree**
1088
1090
  #
1089
- # @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.
1090
1094
  #
1091
- # @param options [Hash] The options hash, checked for `[:repository]`.
1095
+ # @example Get working tree stats since HEAD
1096
+ # repo.diff_stats.insertions #=> 3
1092
1097
  #
1093
- # @param default [String] A fallback path if `options[:repository]` is not set.
1098
+ # @example Compare two specific commits
1099
+ # repo.diff_stats('abc1234', 'def5678')
1094
1100
  #
1095
- # @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
1096
1103
  #
1097
- # @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/')
1098
1106
  #
1099
- private_class_method def self.initial_repository_path(options, default:, bare:)
1100
- if bare
1101
- File.expand_path(options[:repository] || default || Dir.pwd)
1102
- else
1103
- File.expand_path(options[:repository] || '.git', options[:working_directory])
1104
- 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))
1105
1130
  end
1106
1131
 
1107
- # Resolves the path to the actual repository if it's a `gitdir:` pointer file.
1132
+ # Returns the file path status between two commits
1108
1133
  #
1109
- # If `path` points to a file (common in submodules and worktrees), this
1110
- # method reads the `gitdir:` path from it and returns the real repository
1111
- # 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" }
1112
1136
  #
1113
- # @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
1114
1141
  #
1115
- # @param path [String] The initial path to the repository, which may be a pointer file.
1142
+ # @param opts [Hash] options to filter the diff
1116
1143
  #
1117
- # @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)
1118
1146
  #
1119
- # @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
1120
1149
  #
1121
- private_class_method def self.resolve_gitdir_if_present(path, working_dir)
1122
- 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
1123
1161
 
1124
- # The file contains `gitdir: <path>`, so we read the file,
1125
- # extract the path part, and expand it.
1126
- gitdir_pointer = File.read(path).sub(/\Agitdir: /, '').strip
1127
- 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
1128
1176
  end
1129
1177
 
1130
- # 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
1131
1181
  #
1132
- # If options[:index] is a relative directory, convert it to an absolute
1133
- # directory relative to the repository directory
1182
+ # @deprecated Use {#diff_path_status} instead
1134
1183
  #
1135
- private_class_method def self.normalize_index(options)
1136
- index = File.expand_path(options[:index] || 'index', options[:repository])
1137
- 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]
1138
1202
  end
1139
1203
  end
1140
1204
  end