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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy