Skip to content

Commit 4687806

Browse files
committed
fix: fix Rubocop Metrics/ClassLength offense (refactor Git::Log)
The Git::Log class had grown to over 150 lines, making it difficult to read and maintain. This refactoring simplifies the internal implementation to improve its structure and reduce its size to under 100 lines, without altering the public API. The primary motivations were to enhance readability and make the class easier to extend in the future. The key changes include: - Consolidated all query parameters from individual instance variables into a single `@options` hash to simplify state management. - Replaced repetitive builder methods with a concise `set_option` private helper, reducing code duplication. - Centralized the command execution logic into a single `run_log_if_dirty` method to ensure consistent behavior. - Simplified the deprecated Enumerable methods by using a helper for warnings and safe navigation (`&.`) for cleaner error handling. - Modernized the nested `Git::Log::Result` class to use `Data.define`, leveraging Ruby 3.2+ features to create a more concise and immutable result object.
1 parent 0e9d62d commit 4687806

File tree

2 files changed

+98
-224
lines changed

2 files changed

+98
-224
lines changed

.rubocop_todo.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2025-07-06 20:05:03 UTC using RuboCop version 1.77.0.
3+
# on 2025-07-06 21:08:14 UTC using RuboCop version 1.77.0.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
77
# versions of RuboCop, may require this file to be generated again.
88

9-
# Offense count: 3
9+
# Offense count: 2
1010
# Configuration parameters: CountComments, CountAsOne.
1111
Metrics/ClassLength:
1212
Max: 1032

lib/git/log.rb

Lines changed: 96 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,34 @@
11
# frozen_string_literal: true
22

33
module Git
4-
# Return the last n commits that match the specified criteria
4+
# Builds and executes a `git log` query.
55
#
6-
# @example The last (default number) of commits
7-
# git = Git.open('.')
8-
# Git::Log.new(git).execute #=> Enumerable of the last 30 commits
6+
# This class provides a fluent interface for building complex `git log` queries.
7+
# The query is lazily executed when results are requested either via the modern
8+
# `#execute` method or the deprecated Enumerable methods.
99
#
10-
# @example The last n commits
11-
# Git::Log.new(git).max_commits(50).execute #=> Enumerable of last 50 commits
12-
#
13-
# @example All commits returned by `git log`
14-
# Git::Log.new(git).max_count(:all).execute #=> Enumerable of all commits
15-
#
16-
# @example All commits that match complex criteria
17-
# Git::Log.new(git)
18-
# .max_count(:all)
19-
# .object('README.md')
20-
# .since('10 years ago')
21-
# .between('v1.0.7', 'HEAD')
22-
# .execute
10+
# @example Using the modern `execute` API
11+
# log = git.log.max_count(50).between('v1.0', 'v1.1').author('Scott')
12+
# results = log.execute
13+
# puts "Found #{results.size} commits."
14+
# results.each { |commit| puts commit.sha }
2315
#
2416
# @api public
2517
#
2618
class Log
2719
include Enumerable
2820

29-
# An immutable collection of commits returned by Git::Log#execute
30-
#
31-
# This object is an Enumerable that contains Git::Object::Commit objects.
32-
# It provides methods to access the commit data without executing any
33-
# further git commands.
34-
#
21+
# An immutable, Enumerable collection of `Git::Object::Commit` objects.
22+
# Returned by `Git::Log#execute`.
3523
# @api public
36-
class Result
24+
Result = Data.define(:commits) do
3725
include Enumerable
3826

39-
# @private
40-
def initialize(commits)
41-
@commits = commits
42-
end
43-
44-
# @return [Integer] the number of commits in the result set
45-
def size
46-
@commits.size
47-
end
48-
49-
# Iterates over each commit in the result set
50-
#
51-
# @yield [Git::Object::Commit]
52-
def each(&)
53-
@commits.each(&)
54-
end
55-
56-
# @return [Git::Object::Commit, nil] the first commit in the result set
57-
def first
58-
@commits.first
59-
end
60-
61-
# @return [Git::Object::Commit, nil] the last commit in the result set
62-
def last
63-
@commits.last
64-
end
65-
66-
# @param index [Integer] the index of the commit to return
67-
# @return [Git::Object::Commit, nil] the commit at the given index
68-
def [](index)
69-
@commits[index]
70-
end
71-
72-
# @return [String] a string representation of the log
73-
def to_s
74-
map(&:to_s).join("\n")
75-
end
27+
def each(&block) = commits.each(&block)
28+
def last = commits.last
29+
def [](index) = commits[index]
30+
def to_s = map(&:to_s).join("\n")
31+
def size = commits.size
7632
end
7733

7834
# Create a new Git::Log object
@@ -88,12 +44,29 @@ def to_s
8844
# Passing max_count to {#initialize} is equivalent to calling {#max_count} on the object.
8945
#
9046
def initialize(base, max_count = 30)
91-
dirty_log
9247
@base = base
93-
max_count(max_count)
48+
@options = {}
49+
@dirty = true
50+
self.max_count(max_count)
9451
end
9552

96-
# Executes the git log command and returns an immutable result object.
53+
# Set query options using a fluent interface.
54+
# Each method returns `self` to allow for chaining.
55+
#
56+
def max_count(num) = set_option(:count, num == :all ? nil : num)
57+
def all = set_option(:all, true)
58+
def object(objectish) = set_option(:object, objectish)
59+
def author(regex) = set_option(:author, regex)
60+
def grep(regex) = set_option(:grep, regex)
61+
def path(path) = set_option(:path_limiter, path)
62+
def skip(num) = set_option(:skip, num)
63+
def since(date) = set_option(:since, date)
64+
def until(date) = set_option(:until, date)
65+
def between(val1, val2 = nil) = set_option(:between, [val1, val2])
66+
def cherry = set_option(:cherry, true)
67+
def merges = set_option(:merges, true)
68+
69+
# Executes the git log command and returns an immutable result object
9770
#
9871
# This is the preferred way to get log data. It separates the query
9972
# building from the execution, making the API more predictable.
@@ -107,188 +80,89 @@ def initialize(base, max_count = 30)
10780
# end
10881
#
10982
# @return [Git::Log::Result] an object containing the log results
83+
#
11084
def execute
111-
run_log
85+
run_log_if_dirty
11286
Result.new(@commits)
11387
end
11488

115-
# The maximum number of commits to return
116-
#
117-
# @example All commits returned by `git log`
118-
# git = Git.open('.')
119-
# Git::Log.new(git).max_count(:all)
120-
#
121-
# @param num_or_all [Integer, Symbol, nil] the number of commits to return, or
122-
# `:all` or `nil` to return all
123-
#
124-
# @return [self]
125-
#
126-
def max_count(num_or_all)
127-
dirty_log
128-
@max_count = num_or_all == :all ? nil : num_or_all
129-
self
130-
end
131-
132-
# Adds the --all flag to the git log command
133-
#
134-
# This asks for the logs of all refs (basically all commits reachable by HEAD,
135-
# branches, and tags). This does not control the maximum number of commits
136-
# returned. To control how many commits are returned, call {#max_count}.
137-
#
138-
# @example Return the last 50 commits reachable by all refs
139-
# git = Git.open('.')
140-
# Git::Log.new(git).max_count(50).all
141-
#
142-
# @return [self]
143-
#
144-
def all
145-
dirty_log
146-
@all = true
147-
self
148-
end
149-
150-
def object(objectish)
151-
dirty_log
152-
@object = objectish
153-
self
154-
end
155-
156-
def author(regex)
157-
dirty_log
158-
@author = regex
159-
self
160-
end
161-
162-
def grep(regex)
163-
dirty_log
164-
@grep = regex
165-
self
166-
end
167-
168-
def path(path)
169-
dirty_log
170-
@path = path
171-
self
172-
end
173-
174-
def skip(num)
175-
dirty_log
176-
@skip = num
177-
self
178-
end
179-
180-
def since(date)
181-
dirty_log
182-
@since = date
183-
self
184-
end
185-
186-
def until(date)
187-
dirty_log
188-
@until = date
189-
self
190-
end
191-
192-
def between(sha1, sha2 = nil)
193-
dirty_log
194-
@between = [sha1, sha2]
195-
self
196-
end
197-
198-
def cherry
199-
dirty_log
200-
@cherry = true
201-
self
202-
end
203-
204-
def merges
205-
dirty_log
206-
@merges = true
207-
self
208-
end
209-
210-
def to_s
211-
deprecate_method(__method__)
212-
check_log
213-
@commits.map(&:to_s).join("\n")
214-
end
215-
216-
# forces git log to run
217-
218-
def size
219-
deprecate_method(__method__)
220-
check_log
221-
begin
222-
@commits.size
223-
rescue StandardError
224-
nil
225-
end
226-
end
89+
# @!group Deprecated Enumerable Interface
22790

91+
# @deprecated Use {#execute} and call `each` on the result.
22892
def each(&)
229-
deprecate_method(__method__)
230-
check_log
93+
deprecate_and_run
23194
@commits.each(&)
23295
end
23396

234-
def first
235-
deprecate_method(__method__)
236-
check_log
237-
begin
238-
@commits.first
239-
rescue StandardError
240-
nil
241-
end
97+
# @deprecated Use {#execute} and call `size` on the result.
98+
def size
99+
deprecate_and_run
100+
@commits&.size
242101
end
243102

244-
def last
245-
deprecate_method(__method__)
246-
check_log
247-
begin
248-
@commits.last
249-
rescue StandardError
250-
nil
251-
end
103+
# @deprecated Use {#execute} and call `to_s` on the result.
104+
def to_s
105+
deprecate_and_run
106+
@commits&.map(&:to_s)&.join("\n")
252107
end
253108

254-
def [](index)
255-
deprecate_method(__method__)
256-
check_log
257-
begin
258-
@commits[index]
259-
rescue StandardError
260-
nil
109+
# @deprecated Use {#execute} and call the method on the result.
110+
%i[first last []].each do |method_name|
111+
define_method(method_name) do |*args|
112+
deprecate_and_run
113+
@commits&.public_send(method_name, *args)
261114
end
262115
end
263116

264-
private
117+
# @!endgroup
265118

266-
def deprecate_method(method_name)
267-
Git::Deprecation.warn(
268-
"Calling Git::Log##{method_name} is deprecated and will be removed in a future version. " \
269-
"Call #execute and then ##{method_name} on the result object."
270-
)
271-
end
119+
private
272120

273-
def dirty_log
274-
@dirty_flag = true
121+
def set_option(key, value)
122+
@dirty = true
123+
@options[key] = value
124+
self
275125
end
276126

277-
def check_log
278-
return unless @dirty_flag
127+
def run_log_if_dirty
128+
return unless @dirty
279129

280-
run_log
281-
@dirty_flag = false
130+
log_data = @base.lib.full_log_commits(@options)
131+
@commits = log_data.map { |c| Git::Object::Commit.new(@base, c['sha'], c) }
132+
@dirty = false
282133
end
283134

284-
# actually run the 'git log' command
285-
def run_log
286-
log = @base.lib.full_log_commits(
287-
count: @max_count, all: @all, object: @object, path_limiter: @path, since: @since,
288-
author: @author, grep: @grep, skip: @skip, until: @until, between: @between,
289-
cherry: @cherry, merges: @merges
135+
def deprecate_and_run(method = caller_locations(1, 1)[0].label)
136+
Git::Deprecation.warn(
137+
"Calling Git::Log##{method} is deprecated. " \
138+
"Call #execute and then ##{method} on the result object."
290139
)
291-
@commits = log.map { |c| Git::Object::Commit.new(@base, c['sha'], c) }
140+
run_log_if_dirty
292141
end
293142
end
294143
end
144+
145+
fix: fix Rubocop Metrics/ClassLength offense (refactor Git::Log)
146+
147+
The Git::Log class had grown to over 150 lines, making it difficult
148+
to read and maintain. This refactoring simplifies the internal
149+
implementation to improve its structure and reduce its size to under
150+
100 lines, without altering the public API.
151+
152+
The primary motivations were to enhance readability and make the
153+
class easier to extend in the future.
154+
155+
The key changes include:
156+
157+
- Consolidated all query parameters from individual instance
158+
variables into a single `@options` hash to simplify state
159+
management.
160+
- Replaced repetitive builder methods with a concise `set_option`
161+
private helper, reducing code duplication.
162+
- Centralized the command execution logic into a single
163+
`run_log_if_dirty` method to ensure consistent behavior.
164+
- Simplified the deprecated Enumerable methods by using a helper
165+
for warnings and safe navigation (`&.`) for cleaner error handling.
166+
- Modernized the nested `Git::Log::Result` class to use
167+
`Data.define`, leveraging Ruby 3.2+ features to create a more
168+
concise and immutable result object.

0 commit comments

Comments
 (0)