11# frozen_string_literal: true
22
33module 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,64 @@ 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
294143end
0 commit comments