git 3.1.1 → 4.0.1

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.
data/lib/git/log.rb CHANGED
@@ -1,31 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Git
4
-
5
- # Return the last n commits that match the specified criteria
6
- #
7
- # @example The last (default number) of commits
8
- # git = Git.open('.')
9
- # Git::Log.new(git) #=> Enumerable of the last 30 commits
4
+ # Builds and executes a `git log` query.
10
5
  #
11
- # @example The last n commits
12
- # Git::Log.new(git).max_commits(50) #=> Enumerable of last 50 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.
13
9
  #
14
- # @example All commits returned by `git log`
15
- # Git::Log.new(git).max_count(:all) #=> Enumerable of all commits
16
- #
17
- # @example All commits that match complex criteria
18
- # Git::Log.new(git)
19
- # .max_count(:all)
20
- # .object('README.md')
21
- # .since('10 years ago')
22
- # .between('v1.0.7', 'HEAD')
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 }
23
15
  #
24
16
  # @api public
25
17
  #
26
18
  class Log
27
19
  include Enumerable
28
20
 
21
+ # An immutable, Enumerable collection of `Git::Object::Commit` objects.
22
+ # Returned by `Git::Log#execute`.
23
+ # @api public
24
+ Result = Data.define(:commits) do
25
+ include Enumerable
26
+
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
32
+ end
33
+
29
34
  # Create a new Git::Log object
30
35
  #
31
36
  # @example
@@ -39,161 +44,100 @@ module Git
39
44
  # Passing max_count to {#initialize} is equivalent to calling {#max_count} on the object.
40
45
  #
41
46
  def initialize(base, max_count = 30)
42
- dirty_log
43
47
  @base = base
44
- max_count(max_count)
48
+ @options = {}
49
+ @dirty = true
50
+ self.max_count(max_count)
45
51
  end
46
52
 
47
- # The maximum number of commits to return
53
+ # Set query options using a fluent interface.
54
+ # Each method returns `self` to allow for chaining.
48
55
  #
49
- # @example All commits returned by `git log`
50
- # git = Git.open('.')
51
- # Git::Log.new(git).max_count(:all)
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
52
70
  #
53
- # @param num_or_all [Integer, Symbol, nil] the number of commits to return, or
54
- # `:all` or `nil` to return all
55
- #
56
- # @return [self]
57
- #
58
- def max_count(num_or_all)
59
- dirty_log
60
- @max_count = (num_or_all == :all) ? nil : num_or_all
61
- self
62
- end
63
-
64
- # Adds the --all flag to the git log command
71
+ # This is the preferred way to get log data. It separates the query
72
+ # building from the execution, making the API more predictable.
65
73
  #
66
- # This asks for the logs of all refs (basically all commits reachable by HEAD,
67
- # branches, and tags). This does not control the maximum number of commits
68
- # returned. To control how many commits are returned, call {#max_count}.
69
- #
70
- # @example Return the last 50 commits reachable by all refs
71
- # git = Git.open('.')
72
- # Git::Log.new(git).max_count(50).all
74
+ # @example
75
+ # query = g.log.since('2 weeks ago').author('Scott')
76
+ # results = query.execute
77
+ # puts "Found #{results.size} commits"
78
+ # results.each do |commit|
79
+ # # ...
80
+ # end
73
81
  #
74
- # @return [self]
82
+ # @return [Git::Log::Result] an object containing the log results
75
83
  #
76
- def all
77
- dirty_log
78
- @all = true
79
- self
80
- end
81
-
82
- def object(objectish)
83
- dirty_log
84
- @object = objectish
85
- return self
86
- end
87
-
88
- def author(regex)
89
- dirty_log
90
- @author = regex
91
- return self
92
- end
93
-
94
- def grep(regex)
95
- dirty_log
96
- @grep = regex
97
- return self
98
- end
99
-
100
- def path(path)
101
- dirty_log
102
- @path = path
103
- return self
84
+ def execute
85
+ run_log_if_dirty
86
+ Result.new(@commits)
104
87
  end
105
88
 
106
- def skip(num)
107
- dirty_log
108
- @skip = num
109
- return self
110
- end
89
+ # @!group Deprecated Enumerable Interface
111
90
 
112
- def since(date)
113
- dirty_log
114
- @since = date
115
- return self
91
+ # @deprecated Use {#execute} and call `each` on the result.
92
+ def each(&)
93
+ deprecate_and_run
94
+ @commits.each(&)
116
95
  end
117
96
 
118
- def until(date)
119
- dirty_log
120
- @until = date
121
- return self
97
+ # @deprecated Use {#execute} and call `size` on the result.
98
+ def size
99
+ deprecate_and_run
100
+ @commits&.size
122
101
  end
123
102
 
124
- def between(sha1, sha2 = nil)
125
- dirty_log
126
- @between = [sha1, sha2]
127
- return self
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")
128
107
  end
129
108
 
130
- def cherry
131
- dirty_log
132
- @cherry = true
133
- return self
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)
114
+ end
134
115
  end
135
116
 
136
- def merges
137
- dirty_log
138
- @merges = true
139
- return self
140
- end
117
+ # @!endgroup
141
118
 
142
- def to_s
143
- self.map { |c| c.to_s }.join("\n")
144
- end
145
-
146
- # forces git log to run
119
+ private
147
120
 
148
- def size
149
- check_log
150
- @commits.size rescue nil
121
+ def set_option(key, value)
122
+ @dirty = true
123
+ @options[key] = value
124
+ self
151
125
  end
152
126
 
153
- def each(&block)
154
- check_log
155
- @commits.each(&block)
156
- end
127
+ def run_log_if_dirty
128
+ return unless @dirty
157
129
 
158
- def first
159
- check_log
160
- @commits.first rescue nil
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
161
133
  end
162
134
 
163
- def last
164
- check_log
165
- @commits.last rescue nil
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."
139
+ )
140
+ run_log_if_dirty
166
141
  end
167
-
168
- def [](index)
169
- check_log
170
- @commits[index] rescue nil
171
- end
172
-
173
-
174
- private
175
-
176
- def dirty_log
177
- @dirty_flag = true
178
- end
179
-
180
- def check_log
181
- if @dirty_flag
182
- run_log
183
- @dirty_flag = false
184
- end
185
- end
186
-
187
- # actually run the 'git log' command
188
- def run_log
189
- log = @base.lib.full_log_commits(
190
- count: @max_count, all: @all, object: @object, path_limiter: @path, since: @since,
191
- author: @author, grep: @grep, skip: @skip, until: @until, between: @between,
192
- cherry: @cherry, merges: @merges
193
- )
194
- @commits = log.map { |c| Git::Object::Commit.new(@base, c['sha'], c) }
195
- end
196
-
197
142
  end
198
-
199
143
  end
data/lib/git/object.rb CHANGED
@@ -6,10 +6,9 @@ require 'git/errors'
6
6
  require 'git/log'
7
7
 
8
8
  module Git
9
-
10
9
  # represents a git object
11
10
  class Object
12
-
11
+ # A base class for all Git objects
13
12
  class AbstractObject
14
13
  attr_accessor :objectish, :type, :mode
15
14
 
@@ -38,16 +37,16 @@ module Git
38
37
  # read a large file in chunks.
39
38
  #
40
39
  # Use this for large files so that they are not held in memory.
41
- def contents(&block)
40
+ def contents(&)
42
41
  if block_given?
43
- @base.lib.cat_file_contents(@objectish, &block)
42
+ @base.lib.cat_file_contents(@objectish, &)
44
43
  else
45
44
  @contents ||= @base.lib.cat_file_contents(@objectish)
46
45
  end
47
46
  end
48
47
 
49
48
  def contents_array
50
- self.contents.split("\n")
49
+ contents.split("\n")
51
50
  end
52
51
 
53
52
  def to_s
@@ -55,7 +54,7 @@ module Git
55
54
  end
56
55
 
57
56
  def grep(string, path_limiter = nil, opts = {})
58
- opts = {:object => sha, :path_limiter => path_limiter}.merge(opts)
57
+ opts = { object: sha, path_limiter: path_limiter }.merge(opts)
59
58
  @base.lib.grep(string, opts)
60
59
  end
61
60
 
@@ -72,19 +71,17 @@ module Git
72
71
  @base.lib.archive(@objectish, file, opts)
73
72
  end
74
73
 
75
- def tree?; false; end
74
+ def tree? = false
76
75
 
77
- def blob?; false; end
76
+ def blob? = false
78
77
 
79
- def commit?; false; end
80
-
81
- def tag?; false; end
78
+ def commit? = false
82
79
 
80
+ def tag? = false
83
81
  end
84
82
 
85
-
83
+ # A Git blob object
86
84
  class Blob < AbstractObject
87
-
88
85
  def initialize(base, sha, mode = nil)
89
86
  super(base, sha)
90
87
  @mode = mode
@@ -93,11 +90,10 @@ module Git
93
90
  def blob?
94
91
  true
95
92
  end
96
-
97
93
  end
98
94
 
95
+ # A Git tree object
99
96
  class Tree < AbstractObject
100
-
101
97
  def initialize(base, sha, mode = nil)
102
98
  super(base, sha)
103
99
  @mode = mode
@@ -112,13 +108,13 @@ module Git
112
108
  def blobs
113
109
  @blobs ||= check_tree[:blobs]
114
110
  end
115
- alias_method :files, :blobs
111
+ alias files blobs
116
112
 
117
113
  def trees
118
114
  @trees ||= check_tree[:trees]
119
115
  end
120
- alias_method :subtrees, :trees
121
- alias_method :subdirectories, :trees
116
+ alias subtrees trees
117
+ alias subdirectories trees
122
118
 
123
119
  def full_tree
124
120
  @base.lib.full_tree(@objectish)
@@ -134,31 +130,27 @@ module Git
134
130
 
135
131
  private
136
132
 
137
- # actually run the git command
138
- def check_tree
139
- @trees = {}
140
- @blobs = {}
141
-
142
- data = @base.lib.ls_tree(@objectish)
133
+ # actually run the git command
134
+ def check_tree
135
+ @trees = {}
136
+ @blobs = {}
143
137
 
144
- data['tree'].each do |key, tree|
145
- @trees[key] = Git::Object::Tree.new(@base, tree[:sha], tree[:mode])
146
- end
138
+ data = @base.lib.ls_tree(@objectish)
147
139
 
148
- data['blob'].each do |key, blob|
149
- @blobs[key] = Git::Object::Blob.new(@base, blob[:sha], blob[:mode])
150
- end
140
+ data['tree'].each do |key, tree|
141
+ @trees[key] = Git::Object::Tree.new(@base, tree[:sha], tree[:mode])
142
+ end
151
143
 
152
- {
153
- :trees => @trees,
154
- :blobs => @blobs
155
- }
144
+ data['blob'].each do |key, blob|
145
+ @blobs[key] = Git::Object::Blob.new(@base, blob[:sha], blob[:mode])
156
146
  end
157
147
 
148
+ { trees: @trees, blobs: @blobs }
149
+ end
158
150
  end
159
151
 
152
+ # A Git commit object
160
153
  class Commit < AbstractObject
161
-
162
154
  def initialize(base, sha, init = nil)
163
155
  super(base, sha)
164
156
  @tree = nil
@@ -166,9 +158,9 @@ module Git
166
158
  @author = nil
167
159
  @committer = nil
168
160
  @message = nil
169
- if init
170
- set_commit(init)
171
- end
161
+ return unless init
162
+
163
+ from_data(init)
172
164
  end
173
165
 
174
166
  def message
@@ -214,18 +206,23 @@ module Git
214
206
  def committer_date
215
207
  committer.date
216
208
  end
217
- alias_method :date, :committer_date
209
+ alias date committer_date
218
210
 
219
211
  def diff_parent
220
212
  diff(parent)
221
213
  end
222
214
 
223
- def set_commit(data)
215
+ def set_commit(data) # rubocop:disable Naming/AccessorMethodName
216
+ Git.deprecation('Git::Object::Commit#set_commit is deprecated. Use #from_data instead.')
217
+ from_data(data)
218
+ end
219
+
220
+ def from_data(data)
224
221
  @sha ||= data['sha']
225
222
  @committer = Git::Author.new(data['committer'])
226
223
  @author = Git::Author.new(data['author'])
227
224
  @tree = Git::Object::Tree.new(@base, data['tree'])
228
- @parents = data['parent'].map{ |sha| Git::Object::Commit.new(@base, sha) }
225
+ @parents = data['parent'].map { |sha| Git::Object::Commit.new(@base, sha) }
229
226
  @message = data['message'].chomp
230
227
  end
231
228
 
@@ -235,33 +232,57 @@ module Git
235
232
 
236
233
  private
237
234
 
238
- # see if this object has been initialized and do so if not
239
- def check_commit
240
- return if @tree
241
-
242
- data = @base.lib.cat_file_commit(@objectish)
243
- set_commit(data)
244
- end
235
+ # see if this object has been initialized and do so if not
236
+ def check_commit
237
+ return if @tree
245
238
 
239
+ data = @base.lib.cat_file_commit(@objectish)
240
+ from_data(data)
241
+ end
246
242
  end
247
243
 
244
+ # A Git tag object
245
+ #
246
+ # This class represents a tag in Git, which can be either annotated or lightweight.
247
+ #
248
+ # Annotated tags contain additional metadata such as the tagger's name, email, and
249
+ # the date when the tag was created, along with a message.
250
+ #
251
+ # TODO: Annotated tags are not objects
252
+ #
248
253
  class Tag < AbstractObject
249
254
  attr_accessor :name
250
255
 
251
- def initialize(base, sha, name)
256
+ # @overload initialize(base, name)
257
+ # @param base [Git::Base] The Git base object
258
+ # @param name [String] The name of the tag
259
+ #
260
+ # @overload initialize(base, sha, name)
261
+ # @param base [Git::Base] The Git base object
262
+ # @param sha [String] The SHA of the tag object
263
+ # @param name [String] The name of the tag
264
+ #
265
+ def initialize(base, sha, name = nil)
266
+ if name.nil?
267
+ name = sha
268
+ sha = base.lib.tag_sha(name)
269
+ raise Git::UnexpectedResultError, "Tag '#{name}' does not exist." if sha == ''
270
+ end
271
+
252
272
  super(base, sha)
273
+
253
274
  @name = name
254
275
  @annotated = nil
255
276
  @loaded = false
256
277
  end
257
278
 
258
279
  def annotated?
259
- @annotated ||= (@base.lib.cat_file_type(self.name) == 'tag')
280
+ @annotated = @annotated.nil? ? (@base.lib.cat_file_type(name) == 'tag') : @annotated
260
281
  end
261
282
 
262
283
  def message
263
- check_tag()
264
- return @message
284
+ check_tag
285
+ @message
265
286
  end
266
287
 
267
288
  def tag?
@@ -269,8 +290,8 @@ module Git
269
290
  end
270
291
 
271
292
  def tagger
272
- check_tag()
273
- return @tagger
293
+ check_tag
294
+ @tagger
274
295
  end
275
296
 
276
297
  private
@@ -278,31 +299,25 @@ module Git
278
299
  def check_tag
279
300
  return if @loaded
280
301
 
281
- if !self.annotated?
282
- @message = @tagger = nil
283
- else
302
+ if annotated?
284
303
  tdata = @base.lib.cat_file_tag(@name)
285
304
  @message = tdata['message'].chomp
286
305
  @tagger = Git::Author.new(tdata['tagger'])
306
+ else
307
+ @message = @tagger = nil
287
308
  end
288
309
 
289
310
  @loaded = true
290
311
  end
291
-
292
312
  end
293
313
 
294
314
  # if we're calling this, we don't know what type it is yet
295
315
  # so this is our little factory method
296
- def self.new(base, objectish, type = nil, is_tag = false)
297
- if is_tag
298
- sha = base.lib.tag_sha(objectish)
299
- if sha == ''
300
- raise Git::UnexpectedResultError.new("Tag '#{objectish}' does not exist.")
301
- end
302
- return Git::Object::Tag.new(base, sha, objectish)
303
- end
316
+ def self.new(base, objectish, type = nil, is_tag = false) # rubocop:disable Style/OptionalBooleanParameter
317
+ return new_tag(base, objectish) if is_tag
304
318
 
305
319
  type ||= base.lib.cat_file_type(objectish)
320
+ # TODO: why not handle tag case here too?
306
321
  klass =
307
322
  case type
308
323
  when /blob/ then Blob
@@ -312,5 +327,9 @@ module Git
312
327
  klass.new(base, objectish)
313
328
  end
314
329
 
330
+ private_class_method def self.new_tag(base, objectish)
331
+ Git::Deprecation.warn('Git::Object.new with is_tag argument is deprecated. Use Git::Object::Tag.new instead.')
332
+ Git::Object::Tag.new(base, objectish)
333
+ end
315
334
  end
316
335
  end
data/lib/git/path.rb CHANGED
@@ -1,17 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Git
4
+ # A base class that represents and validates a filesystem path
5
+ #
6
+ # Use for tracking things relevant to a Git repository, such as the working
7
+ # directory or index file.
8
+ #
9
+ class Path
10
+ attr_accessor :path
4
11
 
5
- class Path
12
+ def initialize(path, check_path = nil, must_exist: nil)
13
+ unless check_path.nil?
14
+ Git::Deprecation.warn(
15
+ 'The "check_path" argument is deprecated and ' \
16
+ 'will be removed in a future version. Use "must_exist:" instead.'
17
+ )
18
+ end
6
19
 
7
- attr_accessor :path
20
+ # default is true
21
+ must_exist = must_exist.nil? && check_path.nil? ? true : must_exist || check_path
8
22
 
9
- def initialize(path, check_path=true)
10
23
  path = File.expand_path(path)
11
24
 
12
- if check_path && !File.exist?(path)
13
- raise ArgumentError, 'path does not exist', [path]
14
- end
25
+ raise ArgumentError, 'path does not exist', [path] if must_exist && !File.exist?(path)
15
26
 
16
27
  @path = path
17
28
  end
@@ -27,6 +38,5 @@ module Git
27
38
  def to_s
28
39
  @path
29
40
  end
30
- end
31
-
41
+ end
32
42
  end
data/lib/git/remote.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Git
4
- class Remote < Path
5
-
4
+ # A remote in a Git repository
5
+ class Remote
6
6
  attr_accessor :name, :url, :fetch_opts
7
7
 
8
8
  def initialize(base, name)
@@ -13,7 +13,7 @@ module Git
13
13
  @fetch_opts = config['fetch']
14
14
  end
15
15
 
16
- def fetch(opts={})
16
+ def fetch(opts = {})
17
17
  @base.fetch(@name, opts)
18
18
  end
19
19
 
@@ -35,6 +35,5 @@ module Git
35
35
  def to_s
36
36
  @name
37
37
  end
38
-
39
38
  end
40
39
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Git
4
-
5
4
  class Repository < Path
6
5
  end
7
-
8
6
  end