git 4.3.2 → 5.0.0.beta.2

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 (297) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +73 -2705
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/pull_request_template.md +3 -1
  5. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  6. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  7. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  8. data/.github/skills/command-implementation/SKILL.md +229 -0
  9. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  10. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  11. data/.github/skills/dependency-management/SKILL.md +72 -0
  12. data/.github/skills/development-workflow/SKILL.md +506 -0
  13. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  14. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  15. data/.github/skills/extract-facade-from-base-lib/SKILL.md +600 -0
  16. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  17. data/.github/skills/facade-implementation/SKILL.md +274 -0
  18. data/.github/skills/facade-test-conventions/SKILL.md +394 -0
  19. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  20. data/.github/skills/make-skill-template/SKILL.md +176 -0
  21. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  22. data/.github/skills/project-context/SKILL.md +313 -0
  23. data/.github/skills/pull-request-review/SKILL.md +168 -0
  24. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  25. data/.github/skills/release-management/SKILL.md +125 -0
  26. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  27. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  28. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  29. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  30. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  31. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  32. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  33. data/.github/skills/test-debugging/SKILL.md +160 -0
  34. data/.github/skills/yard-documentation/SKILL.md +793 -0
  35. data/.github/workflows/continuous_integration.yml +3 -2
  36. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  37. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  38. data/.github/workflows/release.yml +3 -4
  39. data/.gitignore +8 -0
  40. data/.husky/pre-commit +13 -0
  41. data/.release-please-manifest.json +1 -1
  42. data/.rspec +3 -0
  43. data/.rubocop.yml +12 -3
  44. data/.rubocop_todo.yml +23 -5
  45. data/.yardopts +1 -0
  46. data/CHANGELOG.md +0 -40
  47. data/CONTRIBUTING.md +694 -53
  48. data/README.md +67 -15
  49. data/Rakefile +61 -9
  50. data/UPGRADING.md +141 -0
  51. data/commitlint.test +4 -0
  52. data/git.gemspec +18 -7
  53. data/lib/git/args_builder.rb +0 -8
  54. data/lib/git/branch.rb +369 -43
  55. data/lib/git/branch_delete_failure.rb +31 -0
  56. data/lib/git/branch_delete_result.rb +63 -0
  57. data/lib/git/branch_info.rb +178 -0
  58. data/lib/git/branches.rb +122 -24
  59. data/lib/git/command_line/base.rb +255 -0
  60. data/lib/git/command_line/capturing.rb +251 -0
  61. data/lib/git/command_line/result.rb +96 -0
  62. data/lib/git/command_line/streaming.rb +196 -0
  63. data/lib/git/command_line.rb +43 -322
  64. data/lib/git/command_line_result.rb +4 -88
  65. data/lib/git/commands/add.rb +131 -0
  66. data/lib/git/commands/am/abort.rb +43 -0
  67. data/lib/git/commands/am/apply.rb +252 -0
  68. data/lib/git/commands/am/continue.rb +43 -0
  69. data/lib/git/commands/am/quit.rb +43 -0
  70. data/lib/git/commands/am/retry.rb +47 -0
  71. data/lib/git/commands/am/show_current_patch.rb +64 -0
  72. data/lib/git/commands/am/skip.rb +42 -0
  73. data/lib/git/commands/am.rb +33 -0
  74. data/lib/git/commands/apply.rb +237 -0
  75. data/lib/git/commands/archive/list_formats.rb +46 -0
  76. data/lib/git/commands/archive.rb +140 -0
  77. data/lib/git/commands/arguments.rb +3510 -0
  78. data/lib/git/commands/base.rb +404 -0
  79. data/lib/git/commands/branch/copy.rb +94 -0
  80. data/lib/git/commands/branch/create.rb +173 -0
  81. data/lib/git/commands/branch/delete.rb +80 -0
  82. data/lib/git/commands/branch/list.rb +162 -0
  83. data/lib/git/commands/branch/move.rb +94 -0
  84. data/lib/git/commands/branch/set_upstream.rb +86 -0
  85. data/lib/git/commands/branch/show_current.rb +49 -0
  86. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  87. data/lib/git/commands/branch.rb +34 -0
  88. data/lib/git/commands/cat_file/batch.rb +369 -0
  89. data/lib/git/commands/cat_file/filtered.rb +105 -0
  90. data/lib/git/commands/cat_file/raw.rb +216 -0
  91. data/lib/git/commands/cat_file.rb +49 -0
  92. data/lib/git/commands/checkout/branch.rb +151 -0
  93. data/lib/git/commands/checkout/files.rb +115 -0
  94. data/lib/git/commands/checkout.rb +38 -0
  95. data/lib/git/commands/checkout_index.rb +105 -0
  96. data/lib/git/commands/clean.rb +100 -0
  97. data/lib/git/commands/clone.rb +240 -0
  98. data/lib/git/commands/commit.rb +272 -0
  99. data/lib/git/commands/commit_tree.rb +100 -0
  100. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  101. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  102. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  103. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  104. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  105. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  106. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +107 -0
  107. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  108. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  109. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  110. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  111. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  112. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  113. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  114. data/lib/git/commands/config_option_syntax.rb +56 -0
  115. data/lib/git/commands/describe.rb +155 -0
  116. data/lib/git/commands/diff.rb +656 -0
  117. data/lib/git/commands/diff_files.rb +518 -0
  118. data/lib/git/commands/diff_index.rb +496 -0
  119. data/lib/git/commands/fetch.rb +352 -0
  120. data/lib/git/commands/fsck.rb +136 -0
  121. data/lib/git/commands/gc.rb +132 -0
  122. data/lib/git/commands/grep.rb +338 -0
  123. data/lib/git/commands/init.rb +99 -0
  124. data/lib/git/commands/log.rb +632 -0
  125. data/lib/git/commands/ls_files.rb +191 -0
  126. data/lib/git/commands/ls_remote.rb +155 -0
  127. data/lib/git/commands/ls_tree.rb +131 -0
  128. data/lib/git/commands/maintenance/register.rb +75 -0
  129. data/lib/git/commands/maintenance/run.rb +104 -0
  130. data/lib/git/commands/maintenance/start.rb +66 -0
  131. data/lib/git/commands/maintenance/stop.rb +55 -0
  132. data/lib/git/commands/maintenance/unregister.rb +79 -0
  133. data/lib/git/commands/maintenance.rb +31 -0
  134. data/lib/git/commands/merge/abort.rb +44 -0
  135. data/lib/git/commands/merge/continue.rb +44 -0
  136. data/lib/git/commands/merge/quit.rb +46 -0
  137. data/lib/git/commands/merge/start.rb +245 -0
  138. data/lib/git/commands/merge.rb +28 -0
  139. data/lib/git/commands/merge_base.rb +86 -0
  140. data/lib/git/commands/mv.rb +77 -0
  141. data/lib/git/commands/name_rev.rb +114 -0
  142. data/lib/git/commands/pull.rb +377 -0
  143. data/lib/git/commands/push.rb +246 -0
  144. data/lib/git/commands/read_tree.rb +149 -0
  145. data/lib/git/commands/remote/add.rb +91 -0
  146. data/lib/git/commands/remote/get_url.rb +66 -0
  147. data/lib/git/commands/remote/list.rb +54 -0
  148. data/lib/git/commands/remote/prune.rb +61 -0
  149. data/lib/git/commands/remote/remove.rb +52 -0
  150. data/lib/git/commands/remote/rename.rb +69 -0
  151. data/lib/git/commands/remote/set_branches.rb +63 -0
  152. data/lib/git/commands/remote/set_head.rb +82 -0
  153. data/lib/git/commands/remote/set_url.rb +71 -0
  154. data/lib/git/commands/remote/set_url_add.rb +61 -0
  155. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  156. data/lib/git/commands/remote/show.rb +71 -0
  157. data/lib/git/commands/remote/update.rb +72 -0
  158. data/lib/git/commands/remote.rb +42 -0
  159. data/lib/git/commands/repack.rb +277 -0
  160. data/lib/git/commands/reset.rb +147 -0
  161. data/lib/git/commands/rev_parse.rb +297 -0
  162. data/lib/git/commands/revert/abort.rb +45 -0
  163. data/lib/git/commands/revert/continue.rb +57 -0
  164. data/lib/git/commands/revert/quit.rb +47 -0
  165. data/lib/git/commands/revert/skip.rb +44 -0
  166. data/lib/git/commands/revert/start.rb +153 -0
  167. data/lib/git/commands/revert.rb +29 -0
  168. data/lib/git/commands/rm.rb +114 -0
  169. data/lib/git/commands/show.rb +632 -0
  170. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  171. data/lib/git/commands/show_ref/exists.rb +78 -0
  172. data/lib/git/commands/show_ref/list.rb +145 -0
  173. data/lib/git/commands/show_ref/verify.rb +120 -0
  174. data/lib/git/commands/show_ref.rb +42 -0
  175. data/lib/git/commands/stash/apply.rb +75 -0
  176. data/lib/git/commands/stash/branch.rb +65 -0
  177. data/lib/git/commands/stash/clear.rb +41 -0
  178. data/lib/git/commands/stash/create.rb +58 -0
  179. data/lib/git/commands/stash/drop.rb +67 -0
  180. data/lib/git/commands/stash/list.rb +39 -0
  181. data/lib/git/commands/stash/pop.rb +78 -0
  182. data/lib/git/commands/stash/push.rb +103 -0
  183. data/lib/git/commands/stash/show.rb +149 -0
  184. data/lib/git/commands/stash/store.rb +63 -0
  185. data/lib/git/commands/stash.rb +38 -0
  186. data/lib/git/commands/status.rb +169 -0
  187. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  188. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  189. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  190. data/lib/git/commands/symbolic_ref.rb +38 -0
  191. data/lib/git/commands/tag/create.rb +139 -0
  192. data/lib/git/commands/tag/delete.rb +55 -0
  193. data/lib/git/commands/tag/list.rb +143 -0
  194. data/lib/git/commands/tag/verify.rb +71 -0
  195. data/lib/git/commands/tag.rb +26 -0
  196. data/lib/git/commands/update_ref/batch.rb +140 -0
  197. data/lib/git/commands/update_ref/delete.rb +92 -0
  198. data/lib/git/commands/update_ref/update.rb +106 -0
  199. data/lib/git/commands/update_ref.rb +42 -0
  200. data/lib/git/commands/version.rb +57 -0
  201. data/lib/git/commands/worktree/add.rb +140 -0
  202. data/lib/git/commands/worktree/list.rb +64 -0
  203. data/lib/git/commands/worktree/lock.rb +58 -0
  204. data/lib/git/commands/worktree/management_base.rb +51 -0
  205. data/lib/git/commands/worktree/move.rb +66 -0
  206. data/lib/git/commands/worktree/prune.rb +67 -0
  207. data/lib/git/commands/worktree/remove.rb +63 -0
  208. data/lib/git/commands/worktree/repair.rb +76 -0
  209. data/lib/git/commands/worktree/unlock.rb +47 -0
  210. data/lib/git/commands/worktree.rb +43 -0
  211. data/lib/git/commands/write_tree.rb +68 -0
  212. data/lib/git/commands.rb +87 -0
  213. data/lib/git/config.rb +17 -0
  214. data/lib/git/config_entry_info.rb +106 -0
  215. data/lib/git/configuring.rb +665 -0
  216. data/lib/git/deprecation.rb +9 -0
  217. data/lib/git/detached_head_info.rb +54 -0
  218. data/lib/git/diff.rb +293 -7
  219. data/lib/git/diff_file_numstat_info.rb +29 -0
  220. data/lib/git/diff_file_patch_info.rb +134 -0
  221. data/lib/git/diff_file_raw_info.rb +127 -0
  222. data/lib/git/diff_info.rb +169 -0
  223. data/lib/git/diff_path_status.rb +67 -19
  224. data/lib/git/diff_result.rb +32 -0
  225. data/lib/git/diff_stats.rb +51 -14
  226. data/lib/git/dirstat_info.rb +86 -0
  227. data/lib/git/errors.rb +65 -2
  228. data/lib/git/execution_context/global.rb +31 -0
  229. data/lib/git/execution_context/repository.rb +136 -0
  230. data/lib/git/execution_context.rb +501 -0
  231. data/lib/git/file_ref.rb +74 -0
  232. data/lib/git/fsck_object.rb +9 -9
  233. data/lib/git/fsck_result.rb +1 -1
  234. data/lib/git/log.rb +9 -2
  235. data/lib/git/object.rb +90 -27
  236. data/lib/git/parsers/branch.rb +224 -0
  237. data/lib/git/parsers/cat_file.rb +111 -0
  238. data/lib/git/parsers/config_entry.rb +110 -0
  239. data/lib/git/parsers/diff.rb +585 -0
  240. data/lib/git/parsers/fsck.rb +133 -0
  241. data/lib/git/parsers/grep.rb +42 -0
  242. data/lib/git/parsers/ls_remote.rb +79 -0
  243. data/lib/git/parsers/ls_tree.rb +58 -0
  244. data/lib/git/parsers/stash.rb +208 -0
  245. data/lib/git/parsers/tag.rb +257 -0
  246. data/lib/git/remote.rb +120 -9
  247. data/lib/git/repository/branching.rb +743 -0
  248. data/lib/git/repository/committing.rb +187 -0
  249. data/lib/git/repository/configuring.rb +351 -0
  250. data/lib/git/repository/context_helpers.rb +264 -0
  251. data/lib/git/repository/diffing.rb +775 -0
  252. data/lib/git/repository/factories.rb +682 -0
  253. data/lib/git/repository/inspecting.rb +252 -0
  254. data/lib/git/repository/logging.rb +247 -0
  255. data/lib/git/repository/maintenance.rb +65 -0
  256. data/lib/git/repository/merging.rb +357 -0
  257. data/lib/git/repository/object_operations.rb +1199 -0
  258. data/lib/git/repository/path_resolver.rb +207 -0
  259. data/lib/git/repository/remote_operations.rb +898 -0
  260. data/lib/git/repository/shared_private.rb +51 -0
  261. data/lib/git/repository/staging.rb +554 -0
  262. data/lib/git/repository/stashing.rb +143 -0
  263. data/lib/git/repository/status_operations.rb +201 -0
  264. data/lib/git/repository/worktree_operations.rb +159 -0
  265. data/lib/git/repository.rb +203 -1
  266. data/lib/git/stash.rb +78 -4
  267. data/lib/git/stash_info.rb +104 -0
  268. data/lib/git/stashes.rb +125 -13
  269. data/lib/git/status.rb +215 -18
  270. data/lib/git/tag_delete_failure.rb +31 -0
  271. data/lib/git/tag_delete_result.rb +63 -0
  272. data/lib/git/tag_info.rb +105 -0
  273. data/lib/git/version.rb +109 -2
  274. data/lib/git/version_constraint.rb +81 -0
  275. data/lib/git/worktree.rb +108 -6
  276. data/lib/git/worktrees.rb +94 -7
  277. data/lib/git.rb +260 -61
  278. data/redesign/1_architecture_existing.md +54 -18
  279. data/redesign/2_architecture_redesign.md +365 -46
  280. data/redesign/3_architecture_implementation.md +1497 -63
  281. data/redesign/Phase 4 - Step A.md +360 -0
  282. data/redesign/beta_release.md +107 -0
  283. data/redesign/c1c2_audit.md +566 -0
  284. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  285. data/redesign/config_design.rb +501 -0
  286. data/tasks/gem_tasks.rake +4 -0
  287. data/tasks/npm_tasks.rake +7 -0
  288. data/tasks/rspec.rake +48 -0
  289. data/tasks/test.rake +13 -1
  290. data/tasks/yard.rake +34 -7
  291. metadata +365 -22
  292. data/lib/git/base.rb +0 -1128
  293. data/lib/git/index.rb +0 -6
  294. data/lib/git/lib.rb +0 -2277
  295. data/lib/git/path.rb +0 -38
  296. data/lib/git/working_directory.rb +0 -6
  297. /data/{release-please-config.json → .release-please-config.json} +0 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Value object representing a detached HEAD state
5
+ #
6
+ # When HEAD points directly to a commit rather than a branch reference,
7
+ # the repository is in a "detached HEAD" state. This object captures
8
+ # that state along with the commit SHA that HEAD points to.
9
+ #
10
+ # This class shares a minimal interface with {Git::BranchInfo} to allow
11
+ # polymorphic usage where appropriate:
12
+ # - `short_name` - returns 'HEAD'
13
+ # - `target_oid` - returns the commit SHA
14
+ # - `to_s` - returns 'HEAD'
15
+ # - `detached?` - returns true
16
+ #
17
+ # @example Detecting detached HEAD state
18
+ # head = repo.show_current
19
+ # if head.detached?
20
+ # puts "HEAD detached at #{head.target_oid[0, 7]}"
21
+ # else
22
+ # puts "On branch #{head.short_name}"
23
+ # end
24
+ #
25
+ # @example Polymorphic usage
26
+ # head = repo.show_current
27
+ # puts "Checked out: #{head.short_name}" # Works for both types
28
+ # system("git log #{head.short_name}") # Works for both types
29
+ #
30
+ # @see Git::BranchInfo for the branch counterpart
31
+ # @see Git::Commands::Branch::ShowCurrent for the command that produces this
32
+ #
33
+ # @api public
34
+ #
35
+ # @!attribute [r] target_oid
36
+ #
37
+ # The commit object ID (SHA) that HEAD points to
38
+ #
39
+ # @return [String] the full 40-character object ID
40
+ #
41
+ DetachedHeadInfo = Data.define(:target_oid) do
42
+ # @return [Boolean] always true for DetachedHeadInfo
43
+ def detached? = true
44
+
45
+ # @return [Boolean] always false for DetachedHeadInfo (detached HEAD always has a commit)
46
+ def unborn? = false
47
+
48
+ # @return [String] always 'HEAD'
49
+ def short_name = 'HEAD'
50
+
51
+ # @return [String] always 'HEAD'
52
+ def to_s = 'HEAD'
53
+ end
54
+ end
data/lib/git/diff.rb CHANGED
@@ -4,10 +4,37 @@ require_relative 'diff_path_status'
4
4
  require_relative 'diff_stats'
5
5
 
6
6
  module Git
7
- # object that holds the diff between two commits
7
+ # Diff between two commits or between a commit and the working tree
8
+ #
9
+ # @example Diff between two commits
10
+ # diff = repo.diff('HEAD~1', 'HEAD')
11
+ # diff.size # => 3
12
+ # diff.insertions # => 20
13
+ # diff.deletions # => 5
14
+ #
15
+ # @example Limit diff to a specific path
16
+ # diff = repo.diff('HEAD~1', 'HEAD').path('lib/')
17
+ #
18
+ # @api public
19
+ #
8
20
  class Diff
9
21
  include Enumerable
10
22
 
23
+ # Creates a new Diff
24
+ #
25
+ # @example
26
+ # diff = Git::Diff.new(base, 'HEAD~1', 'HEAD')
27
+ #
28
+ # @param base [Git::Repository] the git repository
29
+ #
30
+ # @param from [String, nil] the starting commit ref, or `nil` to compare
31
+ # from the index
32
+ #
33
+ # @param to [String, nil] the ending commit ref, or `nil` to compare to
34
+ # the working tree
35
+ #
36
+ # @return [void]
37
+ #
11
38
  def initialize(base, from = nil, to = nil)
12
39
  @base = base
13
40
  @from = from&.to_s
@@ -16,7 +43,18 @@ module Git
16
43
  @path = nil
17
44
  @full_diff_files = nil
18
45
  end
19
- attr_reader :from, :to
46
+
47
+ # The starting commit ref
48
+ #
49
+ # @return [String, nil] the starting commit ref, or `nil` if not set
50
+ #
51
+ attr_reader :from
52
+
53
+ # The ending commit ref
54
+ #
55
+ # @return [String, nil] the ending commit ref, or `nil` if not set
56
+ #
57
+ attr_reader :to
20
58
 
21
59
  # Limits the diff to the specified path(s)
22
60
  #
@@ -33,8 +71,11 @@ module Git
33
71
  # @example Remove path filtering (show all files)
34
72
  # diff.path # or diff.path(nil)
35
73
  #
36
- # @param paths [String, Pathname] one or more paths to filter the diff. Pass no arguments to remove filtering.
74
+ # @param paths [String, Pathname] one or more paths to filter the diff;
75
+ # pass no arguments to remove filtering
76
+ #
37
77
  # @return [self] returns self for method chaining
78
+ #
38
79
  # @raise [ArgumentError] if any path is an Array (use splatted arguments instead)
39
80
  #
40
81
  def path(*paths)
@@ -53,21 +94,65 @@ module Git
53
94
  self
54
95
  end
55
96
 
97
+ # Returns the full diff output as a string
98
+ #
99
+ # @example
100
+ # diff.patch # => "diff --git a/file.rb b/file.rb\n..."
101
+ #
102
+ # @return [String] the raw output of `git diff`
103
+ #
56
104
  def patch
57
- @base.lib.diff_full(@from, @to, { path_limiter: @path })
105
+ @base.diff_full(@from, @to, path_limiter: @path)
58
106
  end
59
107
  alias to_s patch
60
108
 
109
+ # Returns the diff file info for the given path
110
+ #
111
+ # @example
112
+ # diff['lib/git.rb'] # => #<Git::Diff::DiffFile ...>
113
+ #
114
+ # @param key [String] the file path to look up
115
+ #
116
+ # @return [Git::Diff::DiffFile] the diff file object for the given path
117
+ #
61
118
  def [](key)
62
119
  process_full
63
120
  @full_diff_files.assoc(key)[1]
64
121
  end
65
122
 
123
+ # Iterates over each changed file in the diff
124
+ #
125
+ # @overload each
126
+ # @example Get an enumerator
127
+ # diff.each.map(&:path) # => ["lib/git.rb", "README.md"]
128
+ #
129
+ # @return [Enumerator<Git::Diff::DiffFile>] an enumerator over the
130
+ # changed files
131
+ #
132
+ # @overload each(&block)
133
+ # @example Iterate with a block
134
+ # diff.each { |file| puts file.path }
135
+ #
136
+ # @yield [file] each changed file in the diff
137
+ #
138
+ # @yieldparam file [Git::Diff::DiffFile] a changed file
139
+ #
140
+ # @yieldreturn [void]
141
+ #
142
+ # @return [Array<Git::Diff::DiffFile>] the array of changed files
143
+ #
66
144
  def each(&)
67
145
  process_full
68
146
  @full_diff_files.map { |file| file[1] }.each(&)
69
147
  end
70
148
 
149
+ # Returns the number of changed files in the diff
150
+ #
151
+ # @example
152
+ # diff.size # => 3
153
+ #
154
+ # @return [Integer] the number of changed files
155
+ #
71
156
  def size
72
157
  stats_provider.total[:files]
73
158
  end
@@ -76,22 +161,61 @@ module Git
76
161
  # DEPRECATED METHODS
77
162
  #
78
163
 
164
+ # Returns the path-to-status hash for all changed files in the diff
165
+ #
166
+ # @example
167
+ # diff.name_status # => { "lib/git.rb" => "M", "README.md" => "A" }
168
+ #
169
+ # @return [Hash<String, String>] map of file path to git status letter
170
+ #
79
171
  def name_status
80
172
  path_status_provider.to_h
81
173
  end
82
174
 
175
+ # Returns the total number of changed lines in the diff
176
+ #
177
+ # @example
178
+ # diff.lines # => 42
179
+ #
180
+ # @return [Integer] the total number of inserted and deleted lines
181
+ #
83
182
  def lines
84
183
  stats_provider.lines
85
184
  end
86
185
 
186
+ # Returns the total number of deleted lines in the diff
187
+ #
188
+ # @example
189
+ # diff.deletions # => 10
190
+ #
191
+ # @return [Integer] the number of deleted lines
192
+ #
87
193
  def deletions
88
194
  stats_provider.deletions
89
195
  end
90
196
 
197
+ # Returns the total number of inserted lines in the diff
198
+ #
199
+ # @example
200
+ # diff.insertions # => 32
201
+ #
202
+ # @return [Integer] the number of inserted lines
203
+ #
91
204
  def insertions
92
205
  stats_provider.insertions
93
206
  end
94
207
 
208
+ # Returns a statistics hash for the diff
209
+ #
210
+ # @example
211
+ # diff.stats
212
+ # # => {
213
+ # # files: { "lib/git.rb" => { insertions: 5, deletions: 2 } },
214
+ # # total: { insertions: 5, deletions: 2, lines: 7 }
215
+ # # }
216
+ #
217
+ # @return [Hash] statistics including per-file and total insert/delete counts
218
+ #
95
219
  def stats
96
220
  {
97
221
  files: stats_provider.files,
@@ -99,13 +223,72 @@ module Git
99
223
  }
100
224
  end
101
225
 
102
- # The changes for a single file within a diff
226
+ # Information about a single changed file within a {Git::Diff}
227
+ #
228
+ # @example Access diff file information
229
+ # diff.each do |file|
230
+ # puts file.path
231
+ # puts file.binary? ? 'binary' : file.patch
232
+ # end
233
+ #
234
+ # @api public
235
+ #
103
236
  class DiffFile
104
- attr_accessor :patch, :path, :mode, :src, :dst, :type
237
+ # The raw diff patch text for this file
238
+ #
239
+ # @return [String, nil] the patch text
240
+ #
241
+ attr_accessor :patch
242
+
243
+ # The file path relative to the repository root
244
+ #
245
+ # @return [String, nil] the file path
246
+ #
247
+ attr_accessor :path
248
+
249
+ # The file mode
250
+ #
251
+ # @return [String] the octal file mode (e.g. `"100644"`)
252
+ #
253
+ attr_accessor :mode
254
+
255
+ # The source (pre-change) blob SHA
256
+ #
257
+ # @return [String] the source blob SHA
258
+ #
259
+ attr_accessor :src
260
+
261
+ # The destination (post-change) blob SHA
262
+ #
263
+ # @return [String] the destination blob SHA
264
+ #
265
+ attr_accessor :dst
266
+
267
+ # The type of change
268
+ #
269
+ # @return [String] the change type (e.g. `"modified"`, `"new"`, `"deleted"`)
270
+ #
271
+ attr_accessor :type
105
272
 
106
273
  @base = nil
274
+
275
+ # Regexp matching a nil blob SHA (all-zero hash of 4 to 40 hex digits)
107
276
  NIL_BLOB_REGEXP = /\A0{4,40}\z/
108
277
 
278
+ # Creates a new DiffFile from parsed diff data
279
+ #
280
+ # @example
281
+ # file = Git::Diff::DiffFile.new(base,
282
+ # patch: "diff --git ...", path: 'lib/git.rb',
283
+ # mode: '100644', src: 'abc123', dst: 'def456',
284
+ # type: 'modified', binary: false)
285
+ #
286
+ # @param base [Git::Repository] the git repository
287
+ #
288
+ # @param hash [Hash] the parsed diff attributes
289
+ #
290
+ # @return [void]
291
+ #
109
292
  def initialize(base, hash)
110
293
  @base = base
111
294
  @patch = hash[:patch]
@@ -117,10 +300,31 @@ module Git
117
300
  @binary = hash[:binary]
118
301
  end
119
302
 
303
+ # Returns true if this file is a binary file
304
+ #
305
+ # @example
306
+ # diff['path/to/image.png'].binary? # => true
307
+ #
308
+ # @return [Boolean] `true` if the file is binary, `false` otherwise
309
+ #
120
310
  def binary?
121
311
  !!@binary
122
312
  end
123
313
 
314
+ # Returns the blob object for this file
315
+ #
316
+ # @example Retrieve the destination blob
317
+ # file.blob # => #<Git::Object::Blob ...>
318
+ #
319
+ # @example Retrieve the source blob
320
+ # file.blob(:src) # => #<Git::Object::Blob ...>
321
+ #
322
+ # @param type [Symbol] `:src` to retrieve the source blob, or `:dst`
323
+ # (default) for the destination blob
324
+ #
325
+ # @return [Git::Object::Blob, nil] the blob object, or `nil` if the blob
326
+ # SHA is the null SHA
327
+ #
124
328
  def blob(type = :dst)
125
329
  if type == :src && !NIL_BLOB_REGEXP.match(@src)
126
330
  @base.object(@src)
@@ -132,6 +336,14 @@ module Git
132
336
 
133
337
  private
134
338
 
339
+ # Validates that no path argument is an Array
340
+ #
341
+ # @param paths [Array] the raw paths array passed to {#path}
342
+ #
343
+ # @return [void]
344
+ #
345
+ # @raise [ArgumentError] if any element of paths is an Array
346
+ #
135
347
  def validate_paths_not_arrays(paths)
136
348
  return unless paths.any?(Array)
137
349
 
@@ -140,27 +352,58 @@ module Git
140
352
  "Use path('lib/', 'docs/') not path(['lib/', 'docs/'])"
141
353
  end
142
354
 
355
+ # Triggers full diff processing if not yet done
356
+ #
357
+ # @return [void]
358
+ #
143
359
  def process_full
144
360
  return if @full_diff_files
145
361
 
146
362
  @full_diff_files = process_full_diff
147
363
  end
148
364
 
365
+ # Returns a memoized DiffPathStatus provider for this diff
366
+ #
367
+ # @return [Git::DiffPathStatus] the path status provider
368
+ #
149
369
  def path_status_provider
150
370
  @path_status_provider ||= Git::DiffPathStatus.new(@base, @from, @to, @path)
151
371
  end
152
372
 
373
+ # Returns a memoized DiffStats provider for this diff
374
+ #
375
+ # @return [Git::DiffStats] the stats provider
376
+ #
153
377
  def stats_provider
154
378
  @stats_provider ||= Git::DiffStats.new(@base, @from, @to, @path)
155
379
  end
156
380
 
381
+ # Parses the full diff output into DiffFile objects
382
+ #
383
+ # @return [Array<Array(String, Git::Diff::DiffFile)>] list of
384
+ # `[filename, DiffFile]` pairs
385
+ #
157
386
  def process_full_diff
158
387
  FullDiffParser.new(@base, patch).parse
159
388
  end
160
389
 
161
- # A private parser class to process the output of `git diff`
390
+ # Private parser for `git diff` output
391
+ #
392
+ # @example Parse a diff patch
393
+ # parser = Git::Diff::FullDiffParser.new(base, patch_text)
394
+ # files = parser.parse
395
+ #
162
396
  # @api private
397
+ #
163
398
  class FullDiffParser
399
+ # Creates a new FullDiffParser
400
+ #
401
+ # @param base [Git::Repository] the git repository
402
+ #
403
+ # @param patch_text [String] the raw `git diff` output to parse
404
+ #
405
+ # @return [void]
406
+ #
164
407
  def initialize(base, patch_text)
165
408
  @base = base
166
409
  @patch_text = patch_text
@@ -169,6 +412,11 @@ module Git
169
412
  @defaults = { mode: '', src: '', dst: '', type: 'modified', binary: false }
170
413
  end
171
414
 
415
+ # Parses the diff text into a list of filename/DiffFile pairs
416
+ #
417
+ # @return [Array<Array(String, Git::Diff::DiffFile)>] list of
418
+ # `[filename, DiffFile]` pairs
419
+ #
172
420
  def parse
173
421
  @patch_text.split("\n").each { |line| process_line(line) }
174
422
  @final_files.map { |filename, data| [filename, DiffFile.new(@base, data)] }
@@ -176,6 +424,12 @@ module Git
176
424
 
177
425
  private
178
426
 
427
+ # Dispatches a single diff line to the appropriate handler
428
+ #
429
+ # @param line [String] a line from the diff output
430
+ #
431
+ # @return [void]
432
+ #
179
433
  def process_line(line)
180
434
  if (new_file_match = line.match(%r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}))
181
435
  start_new_file(new_file_match, line)
@@ -184,12 +438,26 @@ module Git
184
438
  end
185
439
  end
186
440
 
441
+ # Starts tracking a new file from a diff header line
442
+ #
443
+ # @param match [MatchData] the regex match from the diff header line
444
+ #
445
+ # @param line [String] the original diff header line
446
+ #
447
+ # @return [void]
448
+ #
187
449
  def start_new_file(match, line)
188
450
  filename = Git::EscapedPath.new(match[2]).unescape
189
451
  @current_file_data = @defaults.merge({ patch: line, path: filename })
190
452
  @final_files[filename] = @current_file_data
191
453
  end
192
454
 
455
+ # Appends a diff line to the current file's accumulated data
456
+ #
457
+ # @param line [String] a diff line to append
458
+ #
459
+ # @return [void]
460
+ #
193
461
  def append_to_current_file(line)
194
462
  return unless @current_file_data
195
463
 
@@ -200,6 +468,12 @@ module Git
200
468
  @current_file_data[:patch] << "\n#{line}"
201
469
  end
202
470
 
471
+ # Parses an index line to extract source and destination blob SHAs
472
+ #
473
+ # @param line [String] a diff line that may be an index line
474
+ #
475
+ # @return [void]
476
+ #
203
477
  def parse_index_line(line)
204
478
  return unless (match = line.match(/^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/))
205
479
 
@@ -208,6 +482,12 @@ module Git
208
482
  @current_file_data[:mode] = match[3].strip if match[3]
209
483
  end
210
484
 
485
+ # Parses a file mode line to extract the change type and file mode
486
+ #
487
+ # @param line [String] a diff line that may be a file mode line
488
+ #
489
+ # @return [void]
490
+ #
211
491
  def parse_file_mode_line(line)
212
492
  return unless (match = line.match(/^([[:alpha:]]*?) file mode (......)/))
213
493
 
@@ -215,6 +495,12 @@ module Git
215
495
  @current_file_data[:mode] = match[2]
216
496
  end
217
497
 
498
+ # Marks the current file as binary if this is a binary diff line
499
+ #
500
+ # @param line [String] a diff line to check for the binary file marker
501
+ #
502
+ # @return [void]
503
+ #
218
504
  def check_for_binary(line)
219
505
  @current_file_data[:binary] = true if line.match?(/^Binary files /)
220
506
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Immutable value object representing stats for a single file from git numstat output
5
+ #
6
+ # @api public
7
+ #
8
+ # @!attribute [r] path
9
+ # @return [String] the file path (destination path for renames)
10
+ #
11
+ # @!attribute [r] src_path
12
+ # @return [String, nil] the source path for renamed files, nil otherwise
13
+ #
14
+ # @!attribute [r] insertions
15
+ # @return [Integer] number of lines inserted
16
+ #
17
+ # @!attribute [r] deletions
18
+ # @return [Integer] number of lines deleted
19
+ #
20
+ DiffFileNumstatInfo = Data.define(:path, :src_path, :insertions, :deletions) do
21
+ # Check if this file was renamed
22
+ #
23
+ # @return [Boolean] true if the file was renamed
24
+ #
25
+ def renamed?
26
+ !src_path.nil?
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'file_ref'
4
+
5
+ module Git
6
+ # Immutable value object representing a single file's patch information
7
+ #
8
+ # DiffFilePatchInfo encapsulates the parsed data from a unified diff for one file,
9
+ # including source and destination file references, the patch text, change type,
10
+ # and line statistics.
11
+ #
12
+ # @api public
13
+ #
14
+ # @example A modified file patch
15
+ # patch_info = Git::DiffFilePatchInfo.new(
16
+ # src: Git::FileRef.new(mode: '100644', sha: 'abc1234', path: 'lib/foo.rb'),
17
+ # dst: Git::FileRef.new(mode: '100644', sha: 'def5678', path: 'lib/foo.rb'),
18
+ # patch: "diff --git a/lib/foo.rb b/lib/foo.rb\n...",
19
+ # status: :modified,
20
+ # similarity: nil,
21
+ # binary: false,
22
+ # insertions: 10,
23
+ # deletions: 5
24
+ # )
25
+ #
26
+ # @example A new file patch (src is nil)
27
+ # patch_info = Git::DiffFilePatchInfo.new(
28
+ # src: nil,
29
+ # dst: Git::FileRef.new(mode: '100644', sha: 'abc1234', path: 'lib/new.rb'),
30
+ # patch: "diff --git a/lib/new.rb b/lib/new.rb\nnew file mode 100644\n...",
31
+ # status: :added,
32
+ # similarity: nil,
33
+ # binary: false,
34
+ # insertions: 20,
35
+ # deletions: 0
36
+ # )
37
+ #
38
+ # @!attribute [r] src
39
+ # @return [FileRef, nil] the source file reference, or nil for new/added files
40
+ #
41
+ # @!attribute [r] dst
42
+ # @return [FileRef, nil] the destination file reference, or nil for deleted files
43
+ #
44
+ # @!attribute [r] patch
45
+ # @return [String] the full unified diff patch text for this file
46
+ #
47
+ # @!attribute [r] status
48
+ # @return [Symbol] the change status (:added, :modified, :deleted, :renamed, :copied, :type_changed)
49
+ #
50
+ # @!attribute [r] similarity
51
+ # @return [Integer, nil] similarity percentage for renames/copies (0-100), nil otherwise
52
+ #
53
+ # @!attribute [r] binary
54
+ # @return [Boolean] whether this is a binary file
55
+ #
56
+ # @!attribute [r] insertions
57
+ # @return [Integer] number of lines inserted
58
+ #
59
+ # @!attribute [r] deletions
60
+ # @return [Integer] number of lines deleted
61
+ #
62
+ DiffFilePatchInfo = Data.define(
63
+ :src,
64
+ :dst,
65
+ :patch,
66
+ :status,
67
+ :similarity,
68
+ :binary,
69
+ :insertions,
70
+ :deletions
71
+ ) do
72
+ # Get the primary file path
73
+ #
74
+ # Returns the destination path if it exists, otherwise the source path.
75
+ # This is the "current" or "canonical" path for the file.
76
+ #
77
+ # @return [String] the file path
78
+ #
79
+ def path
80
+ dst&.path || src&.path
81
+ end
82
+
83
+ # Get the source file path
84
+ #
85
+ # Returns the source path if it exists. Useful for renames/copies to see
86
+ # where the file came from.
87
+ #
88
+ # @return [String, nil] the source file path, or nil if no source (added files)
89
+ #
90
+ def src_path
91
+ src&.path
92
+ end
93
+
94
+ # Check if this is a binary file
95
+ #
96
+ # @return [Boolean] true if the file is binary
97
+ #
98
+ def binary?
99
+ binary
100
+ end
101
+
102
+ # Check if this file was renamed
103
+ #
104
+ # @return [Boolean]
105
+ #
106
+ def renamed?
107
+ status == :renamed
108
+ end
109
+
110
+ # Check if this file was copied
111
+ #
112
+ # @return [Boolean]
113
+ #
114
+ def copied?
115
+ status == :copied
116
+ end
117
+
118
+ # Check if this file was added
119
+ #
120
+ # @return [Boolean]
121
+ #
122
+ def added?
123
+ status == :added
124
+ end
125
+
126
+ # Check if this file was deleted
127
+ #
128
+ # @return [Boolean]
129
+ #
130
+ def deleted?
131
+ status == :deleted
132
+ end
133
+ end
134
+ end