git 1.19.1 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -1
  3. data/AI_POLICY.md +24 -0
  4. data/CHANGELOG.md +689 -0
  5. data/CODE_OF_CONDUCT.md +25 -0
  6. data/CONTRIBUTING.md +1175 -97
  7. data/GOVERNANCE.md +106 -0
  8. data/LICENSE +1 -1
  9. data/MAINTAINERS.md +17 -4
  10. data/README.md +476 -320
  11. data/UPGRADING.md +1138 -0
  12. data/git.gemspec +124 -36
  13. data/lib/git/author.rb +39 -7
  14. data/lib/git/author_info.rb +66 -0
  15. data/lib/git/branch.rb +615 -65
  16. data/lib/git/branch_delete_failure.rb +34 -0
  17. data/lib/git/branch_delete_result.rb +66 -0
  18. data/lib/git/branch_info.rb +237 -0
  19. data/lib/git/branches.rb +167 -44
  20. data/lib/git/command_line/base.rb +247 -0
  21. data/lib/git/command_line/capturing.rb +308 -0
  22. data/lib/git/command_line/result.rb +88 -0
  23. data/lib/git/command_line/streaming.rb +236 -0
  24. data/lib/git/command_line.rb +52 -0
  25. data/lib/git/commands/add.rb +139 -0
  26. data/lib/git/commands/am/abort.rb +43 -0
  27. data/lib/git/commands/am/apply.rb +263 -0
  28. data/lib/git/commands/am/continue.rb +43 -0
  29. data/lib/git/commands/am/quit.rb +43 -0
  30. data/lib/git/commands/am/retry.rb +49 -0
  31. data/lib/git/commands/am/show_current_patch.rb +64 -0
  32. data/lib/git/commands/am/skip.rb +42 -0
  33. data/lib/git/commands/am.rb +33 -0
  34. data/lib/git/commands/apply.rb +242 -0
  35. data/lib/git/commands/archive/list_formats.rb +46 -0
  36. data/lib/git/commands/archive.rb +145 -0
  37. data/lib/git/commands/arguments.rb +4521 -0
  38. data/lib/git/commands/base.rb +502 -0
  39. data/lib/git/commands/branch/copy.rb +102 -0
  40. data/lib/git/commands/branch/create.rb +177 -0
  41. data/lib/git/commands/branch/delete.rb +88 -0
  42. data/lib/git/commands/branch/list.rb +178 -0
  43. data/lib/git/commands/branch/move.rb +102 -0
  44. data/lib/git/commands/branch/set_upstream.rb +86 -0
  45. data/lib/git/commands/branch/show_current.rb +49 -0
  46. data/lib/git/commands/branch/unset_upstream.rb +53 -0
  47. data/lib/git/commands/branch.rb +34 -0
  48. data/lib/git/commands/cat_file/batch.rb +385 -0
  49. data/lib/git/commands/cat_file/filtered.rb +105 -0
  50. data/lib/git/commands/cat_file/raw.rb +271 -0
  51. data/lib/git/commands/cat_file.rb +49 -0
  52. data/lib/git/commands/checkout/branch.rb +153 -0
  53. data/lib/git/commands/checkout/files.rb +116 -0
  54. data/lib/git/commands/checkout.rb +38 -0
  55. data/lib/git/commands/checkout_index.rb +106 -0
  56. data/lib/git/commands/clean.rb +102 -0
  57. data/lib/git/commands/clone.rb +241 -0
  58. data/lib/git/commands/commit.rb +273 -0
  59. data/lib/git/commands/commit_tree.rb +101 -0
  60. data/lib/git/commands/config_option_syntax/add.rb +86 -0
  61. data/lib/git/commands/config_option_syntax/get.rb +121 -0
  62. data/lib/git/commands/config_option_syntax/get_all.rb +118 -0
  63. data/lib/git/commands/config_option_syntax/get_color.rb +95 -0
  64. data/lib/git/commands/config_option_syntax/get_color_bool.rb +96 -0
  65. data/lib/git/commands/config_option_syntax/get_regexp.rb +119 -0
  66. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +111 -0
  67. data/lib/git/commands/config_option_syntax/list.rb +111 -0
  68. data/lib/git/commands/config_option_syntax/remove_section.rb +79 -0
  69. data/lib/git/commands/config_option_syntax/rename_section.rb +83 -0
  70. data/lib/git/commands/config_option_syntax/replace_all.rb +109 -0
  71. data/lib/git/commands/config_option_syntax/set.rb +119 -0
  72. data/lib/git/commands/config_option_syntax/unset.rb +92 -0
  73. data/lib/git/commands/config_option_syntax/unset_all.rb +94 -0
  74. data/lib/git/commands/config_option_syntax.rb +56 -0
  75. data/lib/git/commands/describe.rb +156 -0
  76. data/lib/git/commands/diff.rb +657 -0
  77. data/lib/git/commands/diff_files.rb +519 -0
  78. data/lib/git/commands/diff_index.rb +499 -0
  79. data/lib/git/commands/fetch.rb +354 -0
  80. data/lib/git/commands/fsck.rb +138 -0
  81. data/lib/git/commands/gc.rb +134 -0
  82. data/lib/git/commands/grep.rb +339 -0
  83. data/lib/git/commands/init.rb +101 -0
  84. data/lib/git/commands/log.rb +634 -0
  85. data/lib/git/commands/ls_files.rb +195 -0
  86. data/lib/git/commands/ls_remote.rb +161 -0
  87. data/lib/git/commands/ls_tree.rb +135 -0
  88. data/lib/git/commands/maintenance/register.rb +77 -0
  89. data/lib/git/commands/maintenance/run.rb +109 -0
  90. data/lib/git/commands/maintenance/start.rb +71 -0
  91. data/lib/git/commands/maintenance/stop.rb +60 -0
  92. data/lib/git/commands/maintenance/unregister.rb +84 -0
  93. data/lib/git/commands/maintenance.rb +31 -0
  94. data/lib/git/commands/merge/abort.rb +44 -0
  95. data/lib/git/commands/merge/continue.rb +44 -0
  96. data/lib/git/commands/merge/quit.rb +46 -0
  97. data/lib/git/commands/merge/start.rb +250 -0
  98. data/lib/git/commands/merge.rb +28 -0
  99. data/lib/git/commands/merge_base.rb +91 -0
  100. data/lib/git/commands/mv.rb +82 -0
  101. data/lib/git/commands/name_rev.rb +119 -0
  102. data/lib/git/commands/pull.rb +382 -0
  103. data/lib/git/commands/push.rb +251 -0
  104. data/lib/git/commands/read_tree.rb +154 -0
  105. data/lib/git/commands/remote/add.rb +96 -0
  106. data/lib/git/commands/remote/get_url.rb +68 -0
  107. data/lib/git/commands/remote/list.rb +56 -0
  108. data/lib/git/commands/remote/prune.rb +63 -0
  109. data/lib/git/commands/remote/remove.rb +52 -0
  110. data/lib/git/commands/remote/rename.rb +76 -0
  111. data/lib/git/commands/remote/set_branches.rb +70 -0
  112. data/lib/git/commands/remote/set_head.rb +89 -0
  113. data/lib/git/commands/remote/set_url.rb +78 -0
  114. data/lib/git/commands/remote/set_url_add.rb +70 -0
  115. data/lib/git/commands/remote/set_url_delete.rb +71 -0
  116. data/lib/git/commands/remote/show.rb +77 -0
  117. data/lib/git/commands/remote/update.rb +79 -0
  118. data/lib/git/commands/remote.rb +42 -0
  119. data/lib/git/commands/repack.rb +281 -0
  120. data/lib/git/commands/reset.rb +154 -0
  121. data/lib/git/commands/rev_parse.rb +304 -0
  122. data/lib/git/commands/revert/abort.rb +45 -0
  123. data/lib/git/commands/revert/continue.rb +62 -0
  124. data/lib/git/commands/revert/quit.rb +47 -0
  125. data/lib/git/commands/revert/skip.rb +44 -0
  126. data/lib/git/commands/revert/start.rb +158 -0
  127. data/lib/git/commands/revert.rb +29 -0
  128. data/lib/git/commands/rm.rb +113 -0
  129. data/lib/git/commands/show.rb +632 -0
  130. data/lib/git/commands/show_ref/exclude_existing.rb +119 -0
  131. data/lib/git/commands/show_ref/exists.rb +80 -0
  132. data/lib/git/commands/show_ref/list.rb +149 -0
  133. data/lib/git/commands/show_ref/verify.rb +122 -0
  134. data/lib/git/commands/show_ref.rb +42 -0
  135. data/lib/git/commands/stash/apply.rb +81 -0
  136. data/lib/git/commands/stash/branch.rb +67 -0
  137. data/lib/git/commands/stash/clear.rb +43 -0
  138. data/lib/git/commands/stash/create.rb +60 -0
  139. data/lib/git/commands/stash/drop.rb +73 -0
  140. data/lib/git/commands/stash/list.rb +43 -0
  141. data/lib/git/commands/stash/pop.rb +87 -0
  142. data/lib/git/commands/stash/push.rb +112 -0
  143. data/lib/git/commands/stash/show.rb +158 -0
  144. data/lib/git/commands/stash/store.rb +72 -0
  145. data/lib/git/commands/stash.rb +38 -0
  146. data/lib/git/commands/status.rb +174 -0
  147. data/lib/git/commands/symbolic_ref/delete.rb +72 -0
  148. data/lib/git/commands/symbolic_ref/read.rb +99 -0
  149. data/lib/git/commands/symbolic_ref/update.rb +79 -0
  150. data/lib/git/commands/symbolic_ref.rb +38 -0
  151. data/lib/git/commands/tag/create.rb +142 -0
  152. data/lib/git/commands/tag/delete.rb +57 -0
  153. data/lib/git/commands/tag/list.rb +146 -0
  154. data/lib/git/commands/tag/verify.rb +71 -0
  155. data/lib/git/commands/tag.rb +26 -0
  156. data/lib/git/commands/update_ref/batch.rb +145 -0
  157. data/lib/git/commands/update_ref/delete.rb +90 -0
  158. data/lib/git/commands/update_ref/update.rb +103 -0
  159. data/lib/git/commands/update_ref.rb +42 -0
  160. data/lib/git/commands/version.rb +60 -0
  161. data/lib/git/commands/worktree/add.rb +139 -0
  162. data/lib/git/commands/worktree/list.rb +64 -0
  163. data/lib/git/commands/worktree/lock.rb +58 -0
  164. data/lib/git/commands/worktree/management_base.rb +51 -0
  165. data/lib/git/commands/worktree/move.rb +66 -0
  166. data/lib/git/commands/worktree/prune.rb +67 -0
  167. data/lib/git/commands/worktree/remove.rb +63 -0
  168. data/lib/git/commands/worktree/repair.rb +76 -0
  169. data/lib/git/commands/worktree/unlock.rb +47 -0
  170. data/lib/git/commands/worktree.rb +43 -0
  171. data/lib/git/commands/write_tree.rb +68 -0
  172. data/lib/git/commands.rb +88 -0
  173. data/lib/git/config.rb +72 -5
  174. data/lib/git/config_entry_info.rb +106 -0
  175. data/lib/git/configuring.rb +795 -0
  176. data/lib/git/detached_head_info.rb +57 -0
  177. data/lib/git/diff.rb +437 -86
  178. data/lib/git/diff_file_numstat_info.rb +31 -0
  179. data/lib/git/diff_file_patch_info.rb +136 -0
  180. data/lib/git/diff_file_raw_info.rb +129 -0
  181. data/lib/git/diff_info.rb +162 -0
  182. data/lib/git/diff_path_status.rb +107 -0
  183. data/lib/git/diff_result.rb +34 -0
  184. data/lib/git/diff_stats.rb +111 -0
  185. data/lib/git/dirstat_info.rb +102 -0
  186. data/lib/git/encoding_utils.rb +32 -1
  187. data/lib/git/errors.rb +285 -0
  188. data/lib/git/escaped_path.rb +57 -5
  189. data/lib/git/execution_context/global.rb +31 -0
  190. data/lib/git/execution_context/repository.rb +151 -0
  191. data/lib/git/execution_context.rb +559 -0
  192. data/lib/git/factories.rb +813 -0
  193. data/lib/git/file_ref.rb +77 -0
  194. data/lib/git/fsck_object.rb +56 -0
  195. data/lib/git/fsck_result.rb +132 -0
  196. data/lib/git/log.rb +306 -90
  197. data/lib/git/object.rb +563 -141
  198. data/lib/git/parsers/branch.rb +240 -0
  199. data/lib/git/parsers/cat_file.rb +111 -0
  200. data/lib/git/parsers/config_entry.rb +110 -0
  201. data/lib/git/parsers/diff.rb +792 -0
  202. data/lib/git/parsers/fsck.rb +144 -0
  203. data/lib/git/parsers/grep.rb +42 -0
  204. data/lib/git/parsers/ls_remote.rb +79 -0
  205. data/lib/git/parsers/ls_tree.rb +58 -0
  206. data/lib/git/parsers/remote.rb +162 -0
  207. data/lib/git/parsers/stash.rb +292 -0
  208. data/lib/git/parsers/status.rb +251 -0
  209. data/lib/git/parsers/tag.rb +341 -0
  210. data/lib/git/parsers/worktree.rb +185 -0
  211. data/lib/git/path_resolver.rb +206 -0
  212. data/lib/git/remote.rb +165 -12
  213. data/lib/git/remote_info.rb +203 -0
  214. data/lib/git/repository/branching.rb +964 -0
  215. data/lib/git/repository/committing.rb +246 -0
  216. data/lib/git/repository/context_helpers.rb +293 -0
  217. data/lib/git/repository/diffing.rb +785 -0
  218. data/lib/git/repository/inspecting.rb +252 -0
  219. data/lib/git/repository/logging.rb +410 -0
  220. data/lib/git/repository/maintenance.rb +65 -0
  221. data/lib/git/repository/merging.rb +451 -0
  222. data/lib/git/repository/object_operations.rb +1551 -0
  223. data/lib/git/repository/remote_operations.rb +984 -0
  224. data/lib/git/repository/shared_private.rb +120 -0
  225. data/lib/git/repository/staging.rb +587 -0
  226. data/lib/git/repository/stashing.rb +623 -0
  227. data/lib/git/repository/status_operations.rb +249 -0
  228. data/lib/git/repository/worktree_operations.rb +339 -0
  229. data/lib/git/repository.rb +484 -2
  230. data/lib/git/stash.rb +109 -12
  231. data/lib/git/stash_info.rb +102 -0
  232. data/lib/git/stashes.rb +169 -26
  233. data/lib/git/status.rb +308 -122
  234. data/lib/git/status_file_info.rb +258 -0
  235. data/lib/git/status_info.rb +189 -0
  236. data/lib/git/tag_delete_failure.rb +34 -0
  237. data/lib/git/tag_delete_result.rb +66 -0
  238. data/lib/git/tag_info.rb +99 -0
  239. data/lib/git/url.rb +15 -8
  240. data/lib/git/version.rb +113 -2
  241. data/lib/git/version_constraint.rb +85 -0
  242. data/lib/git/worktree.rb +150 -8
  243. data/lib/git/worktree_info.rb +128 -0
  244. data/lib/git/worktrees.rb +118 -13
  245. data/lib/git.rb +632 -234
  246. metadata +369 -54
  247. data/.github/stale.yml +0 -25
  248. data/.github/workflows/continuous_integration.yml +0 -49
  249. data/.gitignore +0 -10
  250. data/Dockerfile.changelog-rs +0 -12
  251. data/Gemfile +0 -5
  252. data/ISSUE_TEMPLATE.md +0 -15
  253. data/PULL_REQUEST_TEMPLATE.md +0 -9
  254. data/RELEASING.md +0 -70
  255. data/Rakefile +0 -60
  256. data/lib/git/base/factory.rb +0 -99
  257. data/lib/git/base.rb +0 -711
  258. data/lib/git/command_line_result.rb +0 -86
  259. data/lib/git/failed_error.rb +0 -53
  260. data/lib/git/git_execute_error.rb +0 -7
  261. data/lib/git/index.rb +0 -5
  262. data/lib/git/lib.rb +0 -1328
  263. data/lib/git/path.rb +0 -31
  264. data/lib/git/signaled_error.rb +0 -50
  265. data/lib/git/working_directory.rb +0 -4
@@ -0,0 +1,57 @@
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
+ #
32
+ # @see Git::Commands::Branch::ShowCurrent for the command that produces this
33
+ #
34
+ # @api private
35
+ #
36
+ # Work in progress; this class is internal for now and may be made public in a future release.
37
+ #
38
+ # @!attribute [r] target_oid
39
+ #
40
+ # The commit object ID (SHA) that HEAD points to
41
+ #
42
+ # @return [String] the full 40-character object ID
43
+ #
44
+ DetachedHeadInfo = Data.define(:target_oid) do
45
+ # @return [Boolean] always true for DetachedHeadInfo
46
+ def detached? = true
47
+
48
+ # @return [Boolean] always false for DetachedHeadInfo (detached HEAD always has a commit)
49
+ def unborn? = false
50
+
51
+ # @return [String] always 'HEAD'
52
+ def short_name = 'HEAD'
53
+
54
+ # @return [String] always 'HEAD'
55
+ def to_s = 'HEAD'
56
+ end
57
+ end
data/lib/git/diff.rb CHANGED
@@ -1,79 +1,294 @@
1
- module Git
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'diff_path_status'
4
+ require_relative 'diff_stats'
2
5
 
3
- # object that holds the last X commits on given branch
6
+ module Git
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
+ #
4
20
  class Diff
5
21
  include Enumerable
6
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
+ #
7
38
  def initialize(base, from = nil, to = nil)
8
39
  @base = base
9
- @from = from && from.to_s
10
- @to = to && to.to_s
40
+ @from = from&.to_s
41
+ @to = to&.to_s
11
42
 
12
43
  @path = nil
13
- @full_diff = nil
14
44
  @full_diff_files = nil
15
- @stats = nil
16
45
  end
17
- attr_reader :from, :to
18
46
 
19
- def name_status
20
- cache_name_status
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
58
+
59
+ # Limits the diff to the specified path(s)
60
+ #
61
+ # When called with no arguments (or only nil arguments), removes any existing
62
+ # path filter, showing all files in the diff. Internally stores a single path
63
+ # as a String and multiple paths as an Array for efficiency.
64
+ #
65
+ # @example Limit diff to a single path
66
+ # git.diff('HEAD~3', 'HEAD').path('lib/')
67
+ #
68
+ # @example Limit diff to multiple paths
69
+ # git.diff('HEAD~3', 'HEAD').path('src/', 'docs/', 'README.md')
70
+ #
71
+ # @example Remove path filtering (show all files)
72
+ # diff.path # or diff.path(nil)
73
+ #
74
+ # @param paths [String, Pathname] one or more paths to filter the diff;
75
+ # pass no arguments to remove filtering
76
+ #
77
+ # @return [self] returns self for method chaining
78
+ #
79
+ # @raise [ArgumentError] if any path is an Array (use splatted arguments instead)
80
+ #
81
+ def path(*paths)
82
+ validate_paths_not_arrays(paths)
83
+
84
+ cleaned_paths = paths.compact
85
+
86
+ @path = if cleaned_paths.empty?
87
+ nil
88
+ elsif cleaned_paths.length == 1
89
+ cleaned_paths.first
90
+ else
91
+ cleaned_paths
92
+ end
93
+
94
+ self
95
+ end
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
+ #
104
+ def patch
105
+ @base.diff_full(@from, @to, path_limiter: @path)
106
+ end
107
+ alias to_s patch
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
+ #
118
+ def [](key)
119
+ process_full
120
+ @full_diff_files.assoc(key)[1]
21
121
  end
22
122
 
23
- def path(path)
24
- @path = path
25
- return self
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
+ #
144
+ def each(&)
145
+ process_full
146
+ @full_diff_files.map { |file| file[1] }.each(&)
26
147
  end
27
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
+ #
28
156
  def size
29
- cache_stats
30
- @stats[:total][:files]
157
+ stats_provider.total[:files]
158
+ end
159
+
160
+ #
161
+ # DEPRECATED METHODS
162
+ #
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
+ #
171
+ def name_status
172
+ path_status_provider.to_h
31
173
  end
32
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
+ #
33
182
  def lines
34
- cache_stats
35
- @stats[:total][:lines]
183
+ stats_provider.lines
36
184
  end
37
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
+ #
38
193
  def deletions
39
- cache_stats
40
- @stats[:total][:deletions]
194
+ stats_provider.deletions
41
195
  end
42
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
+ #
43
204
  def insertions
44
- cache_stats
45
- @stats[:total][:insertions]
205
+ stats_provider.insertions
46
206
  end
47
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
+ #
48
219
  def stats
49
- cache_stats
50
- @stats
220
+ {
221
+ files: stats_provider.files,
222
+ total: stats_provider.total
223
+ }
51
224
  end
52
225
 
53
- # if file is provided and is writable, it will write the patch into the file
54
- def patch(file = nil)
55
- cache_full
56
- @full_diff
57
- end
58
- alias_method :to_s, :patch
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
+ #
236
+ class DiffFile
237
+ # The raw diff patch text for this file
238
+ #
239
+ # @return [String, nil] the patch text
240
+ #
241
+ attr_accessor :patch
59
242
 
60
- # enumerable methods
243
+ # The file path relative to the repository root
244
+ #
245
+ # @return [String, nil] the file path
246
+ #
247
+ attr_accessor :path
61
248
 
62
- def [](key)
63
- process_full
64
- @full_diff_files.assoc(key)[1]
65
- end
249
+ # The file mode
250
+ #
251
+ # @return [String] the octal file mode (e.g. `"100644"`)
252
+ #
253
+ attr_accessor :mode
66
254
 
67
- def each(&block) # :yields: each Git::DiffFile in turn
68
- process_full
69
- @full_diff_files.map { |file| file[1] }.each(&block)
70
- end
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
71
272
 
72
- class DiffFile
73
- attr_accessor :patch, :path, :mode, :src, :dst, :type
74
273
  @base = nil
75
- NIL_BLOB_REGEXP = /\A0{4,40}\z/.freeze
76
274
 
275
+ # Regexp matching a nil blob SHA (all-zero hash of 4 to 40 hex digits)
276
+ NIL_BLOB_REGEXP = /\A0{4,40}\z/
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
+ #
77
292
  def initialize(base, hash)
78
293
  @base = base
79
294
  @patch = hash[:patch]
@@ -85,71 +300,207 @@ module Git
85
300
  @binary = hash[:binary]
86
301
  end
87
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
+ #
88
310
  def binary?
89
311
  !!@binary
90
312
  end
91
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
+ #
92
328
  def blob(type = :dst)
93
- if type == :src && !NIL_BLOB_REGEXP.match(@src)
94
- @base.object(@src)
95
- elsif !NIL_BLOB_REGEXP.match(@dst)
96
- @base.object(@dst)
97
- end
329
+ sha = type == :src ? @src : @dst
330
+ @base.object(sha) unless NIL_BLOB_REGEXP.match(sha)
98
331
  end
99
332
  end
100
333
 
101
334
  private
102
335
 
103
- def cache_full
104
- @full_diff ||= @base.lib.diff_full(@from, @to, {:path_limiter => @path})
336
+ # Validates that no path argument is an Array
337
+ #
338
+ # @param paths [Array] the raw paths array passed to {#path}
339
+ #
340
+ # @return [void]
341
+ #
342
+ # @raise [ArgumentError] if any element of paths is an Array
343
+ #
344
+ def validate_paths_not_arrays(paths)
345
+ return unless paths.any?(Array)
346
+
347
+ raise ArgumentError,
348
+ 'path expects individual arguments, not arrays. ' \
349
+ "Use path('lib/', 'docs/') not path(['lib/', 'docs/'])"
350
+ end
351
+
352
+ # Triggers full diff processing if not yet done
353
+ #
354
+ # @return [void]
355
+ #
356
+ def process_full
357
+ return if @full_diff_files
358
+
359
+ @full_diff_files = process_full_diff
360
+ end
361
+
362
+ # Returns a memoized DiffPathStatus provider for this diff
363
+ #
364
+ # @return [Git::DiffPathStatus] the path status provider
365
+ #
366
+ def path_status_provider
367
+ @path_status_provider ||= Git::DiffPathStatus.new(@base, @from, @to, @path)
368
+ end
369
+
370
+ # Returns a memoized DiffStats provider for this diff
371
+ #
372
+ # @return [Git::DiffStats] the stats provider
373
+ #
374
+ def stats_provider
375
+ @stats_provider ||= Git::DiffStats.new(@base, @from, @to, @path)
376
+ end
377
+
378
+ # Parses the full diff output into DiffFile objects
379
+ #
380
+ # @return [Array<Array(String, Git::Diff::DiffFile)>] list of
381
+ # `[filename, DiffFile]` pairs
382
+ #
383
+ def process_full_diff
384
+ FullDiffParser.new(@base, patch).parse
385
+ end
386
+
387
+ # Private parser for `git diff` output
388
+ #
389
+ # @example Parse a diff patch
390
+ # parser = Git::Diff::FullDiffParser.new(base, patch_text)
391
+ # files = parser.parse
392
+ #
393
+ # @api private
394
+ #
395
+ class FullDiffParser
396
+ # Creates a new FullDiffParser
397
+ #
398
+ # @param base [Git::Repository] the git repository
399
+ #
400
+ # @param patch_text [String] the raw `git diff` output to parse
401
+ #
402
+ # @return [void]
403
+ #
404
+ def initialize(base, patch_text)
405
+ @base = base
406
+ @patch_text = patch_text
407
+ @final_files = {}
408
+ @current_file_data = nil
409
+ @defaults = { mode: '', src: '', dst: '', type: 'modified', binary: false }
105
410
  end
106
411
 
107
- def process_full
108
- return if @full_diff_files
109
- cache_full
110
- @full_diff_files = process_full_diff
412
+ # Parses the diff text into a list of filename/DiffFile pairs
413
+ #
414
+ # @return [Array<Array(String, Git::Diff::DiffFile)>] list of
415
+ # `[filename, DiffFile]` pairs
416
+ #
417
+ def parse
418
+ @patch_text.split("\n").each { |line| process_line(line) }
419
+ @final_files.map { |filename, data| [filename, DiffFile.new(@base, data)] }
111
420
  end
112
421
 
113
- def cache_stats
114
- @stats ||= @base.lib.diff_stats(@from, @to, {:path_limiter => @path})
422
+ private
423
+
424
+ # Dispatches a single diff line to the appropriate handler
425
+ #
426
+ # @param line [String] a line from the diff output
427
+ #
428
+ # @return [void]
429
+ #
430
+ def process_line(line)
431
+ if (new_file_match = line.match(%r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}))
432
+ start_new_file(new_file_match, line)
433
+ else
434
+ append_to_current_file(line)
435
+ end
115
436
  end
116
437
 
117
- def cache_name_status
118
- @name_status ||= @base.lib.diff_name_status(@from, @to, {:path => @path})
438
+ # Starts tracking a new file from a diff header line
439
+ #
440
+ # @param match [MatchData] the regex match from the diff header line
441
+ #
442
+ # @param line [String] the original diff header line
443
+ #
444
+ # @return [void]
445
+ #
446
+ def start_new_file(match, line)
447
+ filename = Git::EscapedPath.new(match[2]).unescape
448
+ @current_file_data = @defaults.merge({ patch: line, path: filename })
449
+ @final_files[filename] = @current_file_data
119
450
  end
120
451
 
121
- # break up @diff_full
122
- def process_full_diff
123
- defaults = {
124
- :mode => '',
125
- :src => '',
126
- :dst => '',
127
- :type => 'modified'
128
- }
129
- final = {}
130
- current_file = nil
131
- @full_diff.split("\n").each do |line|
132
- if m = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}.match(line)
133
- current_file = Git::EscapedPath.new(m[2]).unescape
134
- final[current_file] = defaults.merge({:patch => line, :path => current_file})
135
- else
136
- if m = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/.match(line)
137
- final[current_file][:src] = m[1]
138
- final[current_file][:dst] = m[2]
139
- final[current_file][:mode] = m[3].strip if m[3]
140
- end
141
- if m = /^([[:alpha:]]*?) file mode (......)/.match(line)
142
- final[current_file][:type] = m[1]
143
- final[current_file][:mode] = m[2]
144
- end
145
- if m = /^Binary files /.match(line)
146
- final[current_file][:binary] = true
147
- end
148
- final[current_file][:patch] << "\n" + line
149
- end
150
- end
151
- final.map { |e| [e[0], DiffFile.new(@base, e[1])] }
452
+ # Appends a diff line to the current file's accumulated data
453
+ #
454
+ # @param line [String] a diff line to append
455
+ #
456
+ # @return [void]
457
+ #
458
+ def append_to_current_file(line)
459
+ return unless @current_file_data
460
+
461
+ parse_index_line(line)
462
+ parse_file_mode_line(line)
463
+ check_for_binary(line)
464
+
465
+ @current_file_data[:patch] << "\n#{line}"
152
466
  end
153
467
 
468
+ # Parses an index line to extract source and destination blob SHAs
469
+ #
470
+ # @param line [String] a diff line that may be an index line
471
+ #
472
+ # @return [void]
473
+ #
474
+ def parse_index_line(line)
475
+ return unless (match = line.match(/^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/))
476
+
477
+ @current_file_data[:src] = match[1]
478
+ @current_file_data[:dst] = match[2]
479
+ @current_file_data[:mode] = match[3].strip if match[3]
480
+ end
481
+
482
+ # Parses a file mode line to extract the change type and file mode
483
+ #
484
+ # @param line [String] a diff line that may be a file mode line
485
+ #
486
+ # @return [void]
487
+ #
488
+ def parse_file_mode_line(line)
489
+ return unless (match = line.match(/^([[:alpha:]]*?) file mode (......)/))
490
+
491
+ @current_file_data[:type] = match[1]
492
+ @current_file_data[:mode] = match[2]
493
+ end
494
+
495
+ # Marks the current file as binary if this is a binary diff line
496
+ #
497
+ # @param line [String] a diff line to check for the binary file marker
498
+ #
499
+ # @return [void]
500
+ #
501
+ def check_for_binary(line)
502
+ @current_file_data[:binary] = true if line.match?(/^Binary files /)
503
+ end
504
+ end
154
505
  end
155
506
  end
@@ -0,0 +1,31 @@
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 private
7
+ #
8
+ # Work in progress; this class is internal for now and may be made public in a future release.
9
+ #
10
+ # @!attribute [r] path
11
+ # @return [String] the file path (destination path for renames)
12
+ #
13
+ # @!attribute [r] src_path
14
+ # @return [String, nil] the source path for renamed files, nil otherwise
15
+ #
16
+ # @!attribute [r] insertions
17
+ # @return [Integer] number of lines inserted
18
+ #
19
+ # @!attribute [r] deletions
20
+ # @return [Integer] number of lines deleted
21
+ #
22
+ DiffFileNumstatInfo = Data.define(:path, :src_path, :insertions, :deletions) do
23
+ # Check if this file was renamed
24
+ #
25
+ # @return [Boolean] true if the file was renamed
26
+ #
27
+ def renamed?
28
+ !src_path.nil?
29
+ end
30
+ end
31
+ end