Skip to content

Commit e22eb10

Browse files
committed
feat(diff): refactor Git::Diff to separate concerns and improve AP
This pull request refactors the Git::Diff decomposing it into new, more focused classes, while backward compatibility is maintained via a deprecated facade.
1 parent ded54c4 commit e22eb10

File tree

8 files changed

+300
-82
lines changed

8 files changed

+300
-82
lines changed

lib/git/base.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,27 @@ def merge_base(*args)
782782
shas.map { |sha| gcommit(sha) }
783783
end
784784

785+
# Returns a Git::Diff::Stats object for accessing diff statistics.
786+
#
787+
# @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
788+
# @param obj2 [String, nil] The second commit or object to compare.
789+
# @return [Git::Diff::Stats]
790+
def diff_stats(objectish = 'HEAD', obj2 = nil)
791+
Git::DiffStats.new(self, objectish, obj2)
792+
end
793+
794+
# Returns a Git::Diff::PathStatus object for accessing the name-status report.
795+
#
796+
# @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
797+
# @param obj2 [String, nil] The second commit or object to compare.
798+
# @return [Git::Diff::PathStatus]
799+
def diff_path_status(objectish = 'HEAD', obj2 = nil)
800+
Git::DiffPathStatus.new(self, objectish, obj2)
801+
end
802+
803+
# Provided for backwards compatibility
804+
alias diff_name_status diff_path_status
805+
785806
private
786807

787808
# Normalize options before they are sent to Git::Base.new

lib/git/diff.rb

Lines changed: 79 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# frozen_string_literal: true
22

3-
module Git
3+
require_relative 'diff_path_status'
4+
require_relative 'diff_stats'
45

5-
# object that holds the last X commits on given branch
6+
module Git
7+
# object that holds the diff between two commits
68
class Diff
79
include Enumerable
810

@@ -12,63 +14,68 @@ def initialize(base, from = nil, to = nil)
1214
@to = to && to.to_s
1315

1416
@path = nil
15-
@full_diff = nil
1617
@full_diff_files = nil
17-
@stats = nil
1818
end
1919
attr_reader :from, :to
2020

21-
def name_status
22-
cache_name_status
23-
end
24-
2521
def path(path)
2622
@path = path
2723
self
2824
end
2925

30-
def size
31-
cache_stats
32-
@stats[:total][:files]
26+
def patch
27+
@base.lib.diff_full(@from, @to, { path_limiter: @path })
3328
end
29+
alias_method :to_s, :patch
3430

35-
def lines
36-
cache_stats
37-
@stats[:total][:lines]
31+
def [](key)
32+
process_full
33+
@full_diff_files.assoc(key)[1]
3834
end
3935

40-
def deletions
41-
cache_stats
42-
@stats[:total][:deletions]
36+
def each(&block)
37+
process_full
38+
@full_diff_files.map { |file| file[1] }.each(&block)
4339
end
4440

45-
def insertions
46-
cache_stats
47-
@stats[:total][:insertions]
41+
#
42+
# DEPRECATED METHODS
43+
#
44+
45+
def name_status
46+
Git::Deprecation.warn("Git::Diff#name_status is deprecated. Use Git::Base#diff_path_status instead.")
47+
path_status_provider.to_h
4848
end
4949

50-
def stats
51-
cache_stats
52-
@stats
50+
def size
51+
Git::Deprecation.warn("Git::Diff#size is deprecated. Use Git::Base#diff_stats(...).total[:files] instead.")
52+
stats_provider.total[:files]
5353
end
5454

55-
# if file is provided and is writable, it will write the patch into the file
56-
def patch(file = nil)
57-
cache_full
58-
@full_diff
55+
56+
57+
def lines
58+
Git::Deprecation.warn("Git::Diff#lines is deprecated. Use Git::Base#diff_stats(...).lines instead.")
59+
stats_provider.lines
5960
end
60-
alias_method :to_s, :patch
6161

62-
# enumerable methods
62+
def deletions
63+
Git::Deprecation.warn("Git::Diff#deletions is deprecated. Use Git::Base#diff_stats(...).deletions instead.")
64+
stats_provider.deletions
65+
end
6366

64-
def [](key)
65-
process_full
66-
@full_diff_files.assoc(key)[1]
67+
def insertions
68+
Git::Deprecation.warn("Git::Diff#insertions is deprecated. Use Git::Base#diff_stats(...).insertions instead.")
69+
stats_provider.insertions
6770
end
6871

69-
def each(&block) # :yields: each Git::DiffFile in turn
70-
process_full
71-
@full_diff_files.map { |file| file[1] }.each(&block)
72+
def stats
73+
Git::Deprecation.warn("Git::Diff#stats is deprecated. Use Git::Base#diff_stats instead.")
74+
# CORRECTED: Re-create the original hash structure for backward compatibility
75+
{
76+
files: stats_provider.files,
77+
total: stats_provider.total
78+
}
7279
end
7380

7481
class DiffFile
@@ -102,56 +109,48 @@ def blob(type = :dst)
102109

103110
private
104111

105-
def cache_full
106-
@full_diff ||= @base.lib.diff_full(@from, @to, {:path_limiter => @path})
107-
end
108-
109-
def process_full
110-
return if @full_diff_files
111-
cache_full
112-
@full_diff_files = process_full_diff
113-
end
112+
def process_full
113+
return if @full_diff_files
114+
@full_diff_files = process_full_diff
115+
end
114116

115-
def cache_stats
116-
@stats ||= @base.lib.diff_stats(@from, @to, {:path_limiter => @path})
117-
end
117+
# CORRECTED: Pass the @path variable to the new objects
118+
def path_status_provider
119+
@path_status_provider ||= Git::DiffPathStatus.new(@base, @from, @to, @path)
120+
end
118121

119-
def cache_name_status
120-
@name_status ||= @base.lib.diff_name_status(@from, @to, {:path => @path})
121-
end
122+
# CORRECTED: Pass the @path variable to the new objects
123+
def stats_provider
124+
@stats_provider ||= Git::DiffStats.new(@base, @from, @to, @path)
125+
end
122126

123-
# break up @diff_full
124-
def process_full_diff
125-
defaults = {
126-
:mode => '',
127-
:src => '',
128-
:dst => '',
129-
:type => 'modified'
130-
}
131-
final = {}
132-
current_file = nil
133-
@full_diff.split("\n").each do |line|
134-
if m = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}.match(line)
135-
current_file = Git::EscapedPath.new(m[2]).unescape
136-
final[current_file] = defaults.merge({:patch => line, :path => current_file})
137-
else
138-
if m = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/.match(line)
139-
final[current_file][:src] = m[1]
140-
final[current_file][:dst] = m[2]
141-
final[current_file][:mode] = m[3].strip if m[3]
142-
end
143-
if m = /^([[:alpha:]]*?) file mode (......)/.match(line)
144-
final[current_file][:type] = m[1]
145-
final[current_file][:mode] = m[2]
146-
end
147-
if m = /^Binary files /.match(line)
148-
final[current_file][:binary] = true
149-
end
150-
final[current_file][:patch] << "\n" + line
127+
def process_full_diff
128+
defaults = {
129+
mode: '', src: '', dst: '', type: 'modified'
130+
}
131+
final = {}
132+
current_file = nil
133+
patch.split("\n").each do |line|
134+
if m = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}.match(line)
135+
current_file = Git::EscapedPath.new(m[2]).unescape
136+
final[current_file] = defaults.merge({ patch: line, path: current_file })
137+
else
138+
if m = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/.match(line)
139+
final[current_file][:src] = m[1]
140+
final[current_file][:dst] = m[2]
141+
final[current_file][:mode] = m[3].strip if m[3]
142+
end
143+
if m = /^([[:alpha:]]*?) file mode (......)/.match(line)
144+
final[current_file][:type] = m[1]
145+
final[current_file][:mode] = m[2]
146+
end
147+
if m = /^Binary files /.match(line)
148+
final[current_file][:binary] = true
151149
end
150+
final[current_file][:patch] << "\n" + line
152151
end
153-
final.map { |e| [e[0], DiffFile.new(@base, e[1])] }
154152
end
155-
153+
final.map { |e| [e[0], DiffFile.new(@base, e[1])] }
154+
end
156155
end
157156
end

lib/git/diff_path_status.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
module Git
4+
class DiffPathStatus
5+
include Enumerable
6+
7+
# @private
8+
def initialize(base, from, to, path_limiter = nil)
9+
# Eagerly check for invalid arguments
10+
[from, to].compact.each do |arg|
11+
raise ArgumentError, "Invalid argument: '#{arg}'" if arg.start_with?('-')
12+
end
13+
14+
@base = base
15+
@from = from
16+
@to = to
17+
@path_limiter = path_limiter
18+
@path_status = nil
19+
end
20+
21+
# Iterates over each file's status.
22+
#
23+
# @yield [path, status]
24+
def each(&block)
25+
fetch_path_status.each(&block)
26+
end
27+
28+
# Returns the name-status report as a Hash.
29+
#
30+
# @return [Hash<String, String>] A hash where keys are file paths
31+
# and values are their status codes.
32+
def to_h
33+
fetch_path_status
34+
end
35+
36+
private
37+
38+
# Lazily fetches and caches the path status from the git lib.
39+
def fetch_path_status
40+
@path_status ||= @base.lib.diff_path_status(
41+
@from, @to, { path: @path_limiter }
42+
)
43+
end
44+
end
45+
end

lib/git/diff_stats.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
module Git
4+
# Provides access to the statistics of a diff between two commits,
5+
# including insertions, deletions, and file-level details.
6+
class DiffStats
7+
# @private
8+
def initialize(base, from, to, path_limiter = nil)
9+
# Eagerly check for invalid arguments
10+
[from, to].compact.each do |arg|
11+
raise ArgumentError, "Invalid argument: '#{arg}'" if arg.start_with?('-')
12+
end
13+
14+
@base = base
15+
@from = from
16+
@to = to
17+
@path_limiter = path_limiter
18+
@stats = nil
19+
end
20+
21+
# Returns the total number of lines deleted.
22+
def deletions
23+
fetch_stats[:total][:deletions]
24+
end
25+
26+
# Returns the total number of lines inserted.
27+
def insertions
28+
fetch_stats[:total][:insertions]
29+
end
30+
31+
# Returns the total number of lines changed (insertions + deletions).
32+
def lines
33+
fetch_stats[:total][:lines]
34+
end
35+
36+
# Returns a hash of statistics for each file in the diff.
37+
#
38+
# @return [Hash<String, {insertions: Integer, deletions: Integer}>]
39+
def files
40+
fetch_stats[:files]
41+
end
42+
43+
# Returns a hash of the total statistics for the diff.
44+
#
45+
# @return [{insertions: Integer, deletions: Integer, lines: Integer, files: Integer}]
46+
def total
47+
fetch_stats[:total]
48+
end
49+
50+
private
51+
52+
# Lazily fetches and caches the stats from the git lib.
53+
def fetch_stats
54+
@stats ||= @base.lib.diff_stats(
55+
@from, @to, { path_limiter: @path_limiter }
56+
)
57+
end
58+
end
59+
end

lib/git/lib.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,7 +848,7 @@ def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
848848
hsh
849849
end
850850

851-
def diff_name_status(reference1 = nil, reference2 = nil, opts = {})
851+
def diff_path_status(reference1 = nil, reference2 = nil, opts = {})
852852
assert_args_are_not_options('commit or commit range', reference1, reference2)
853853

854854
opts_arr = ['--name-status']

tests/units/test_diff.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def test_diff_patch_with_bad_commit
128128
end
129129
end
130130

131-
def test_diff_name_status_with_bad_commit
131+
def test_diff_path_status_with_bad_commit
132132
assert_raise(ArgumentError) do
133133
@git.diff('-s').name_status
134134
end

0 commit comments

Comments
 (0)