git 4.0.0 → 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/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
data/lib/git/stash.rb CHANGED
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Git
4
+ # A stash in a Git repository
4
5
  class Stash
6
+ def initialize(base, message, existing = nil, save: nil)
7
+ unless existing.nil?
8
+ Git::Deprecation.warn(
9
+ 'The "existing" argument is deprecated and will be removed in a future version. Use "save:" instead.'
10
+ )
11
+ end
12
+
13
+ # default is false
14
+ save = existing.nil? && save.nil? ? false : save | existing
5
15
 
6
- def initialize(base, message, existing=false)
7
16
  @base = base
8
17
  @message = message
9
- save unless existing
18
+ self.save unless save
10
19
  end
11
20
 
12
21
  def save
@@ -17,12 +26,10 @@ module Git
17
26
  @saved
18
27
  end
19
28
 
20
- def message
21
- @message
22
- end
29
+ attr_reader :message
23
30
 
24
31
  def to_s
25
32
  message
26
33
  end
27
34
  end
28
- end
35
+ end
data/lib/git/stashes.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Git
4
-
5
4
  # object that holds all the available stashes
6
5
  class Stashes
7
6
  include Enumerable
@@ -11,7 +10,8 @@ module Git
11
10
 
12
11
  @base = base
13
12
 
14
- @base.lib.stashes_all.each do |id, message|
13
+ @base.lib.stashes_all.each do |indexed_message|
14
+ _index, message = indexed_message
15
15
  @stashes.unshift(Git::Stash.new(@base, message, true))
16
16
  end
17
17
  end
@@ -32,7 +32,7 @@ module Git
32
32
  @stashes.unshift(s) if s.saved?
33
33
  end
34
34
 
35
- def apply(index=nil)
35
+ def apply(index = nil)
36
36
  @base.lib.stash_apply(index)
37
37
  end
38
38
 
@@ -45,8 +45,8 @@ module Git
45
45
  @stashes.size
46
46
  end
47
47
 
48
- def each(&block)
49
- @stashes.each(&block)
48
+ def each(&)
49
+ @stashes.each(&)
50
50
  end
51
51
 
52
52
  def [](index)
data/lib/git/status.rb CHANGED
@@ -1,115 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # These would be required by the main `git.rb` file
4
+
3
5
  module Git
4
- # The status class gets the status of a git repository
5
- #
6
- # This identifies which files have been modified, added, or deleted from the
7
- # worktree. Untracked files are also identified.
8
- #
9
- # The Status object is an Enumerable that contains StatusFile objects.
6
+ # The Status class gets the status of a git repository. It identifies which
7
+ # files have been modified, added, or deleted, including untracked files.
8
+ # The Status object is an Enumerable of StatusFile objects.
10
9
  #
11
10
  # @api public
12
11
  #
13
12
  class Status
14
13
  include Enumerable
15
14
 
15
+ # @param base [Git::Base] The base git object
16
16
  def initialize(base)
17
17
  @base = base
18
- construct_status
19
- end
20
-
21
- #
22
- # Returns an Enumerable containing files that have changed from the
23
- # git base directory
24
- #
25
- # @return [Enumerable]
26
- def changed
27
- @_changed ||= @files.select { |_k, f| f.type == 'M' }
28
- end
29
-
30
- #
31
- # Determines whether the given file has been changed.
32
- # File path starts at git base directory
33
- #
34
- # @param file [String] The name of the file.
35
- # @example Check if lib/git.rb has changed.
36
- # changed?('lib/git.rb')
37
- # @return [Boolean]
38
- def changed?(file)
39
- case_aware_include?(:changed, :lc_changed, file)
40
- end
41
-
42
- # Returns an Enumerable containing files that have been added.
43
- # File path starts at git base directory
44
- #
45
- # @return [Enumerable]
46
- def added
47
- @_added ||= @files.select { |_k, f| f.type == 'A' }
48
- end
49
-
50
- # Determines whether the given file has been added to the repository
51
- #
52
- # File path starts at git base directory
53
- #
54
- # @param file [String] The name of the file.
55
- # @example Check if lib/git.rb is added.
56
- # added?('lib/git.rb')
57
- # @return [Boolean]
58
- def added?(file)
59
- case_aware_include?(:added, :lc_added, file)
18
+ # The factory returns a hash of file paths to StatusFile objects.
19
+ @files = StatusFileFactory.new(base).construct_files
60
20
  end
61
21
 
62
- #
63
- # Returns an Enumerable containing files that have been deleted.
64
- # File path starts at git base directory
65
- #
66
- # @return [Enumerable]
67
- def deleted
68
- @_deleted ||= @files.select { |_k, f| f.type == 'D' }
69
- end
22
+ # File status collections, memoized for performance.
23
+ def changed = @changed ||= select_files { |f| f.type == 'M' }
24
+ def added = @added ||= select_files { |f| f.type == 'A' }
25
+ def deleted = @deleted ||= select_files { |f| f.type == 'D' }
26
+ # This works with `true` or `nil`
27
+ def untracked = @untracked ||= select_files(&:untracked)
70
28
 
71
- #
72
- # Determines whether the given file has been deleted from the repository
73
- # File path starts at git base directory
74
- #
75
- # @param file [String] The name of the file.
76
- # @example Check if lib/git.rb is deleted.
77
- # deleted?('lib/git.rb')
78
- # @return [Boolean]
79
- def deleted?(file)
80
- case_aware_include?(:deleted, :lc_deleted, file)
81
- end
82
-
83
- #
84
- # Returns an Enumerable containing files that are not tracked in git.
85
- # File path starts at git base directory
86
- #
87
- # @return [Enumerable]
88
- def untracked
89
- @_untracked ||= @files.select { |_k, f| f.untracked }
90
- end
29
+ # Predicate methods to check the status of a specific file.
30
+ def changed?(file) = file_in_collection?(:changed, file)
31
+ def added?(file) = file_in_collection?(:added, file)
32
+ def deleted?(file) = file_in_collection?(:deleted, file)
33
+ def untracked?(file) = file_in_collection?(:untracked, file)
91
34
 
92
- #
93
- # Determines whether the given file has is tracked by git.
94
- # File path starts at git base directory
95
- #
96
- # @param file [String] The name of the file.
97
- # @example Check if lib/git.rb is an untracked file.
98
- # untracked?('lib/git.rb')
99
- # @return [Boolean]
100
- def untracked?(file)
101
- case_aware_include?(:untracked, :lc_untracked, file)
102
- end
35
+ # Access a status file by path, or iterate over all status files.
36
+ def [](file) = @files[file]
37
+ def each(&) = @files.values.each(&)
103
38
 
39
+ # Returns a formatted string representation of the status.
104
40
  def pretty
105
- out = +''
106
- each do |file|
107
- out << pretty_file(file)
108
- end
109
- out << "\n"
110
- out
41
+ map { |file| pretty_file(file) }.join << "\n"
111
42
  end
112
43
 
44
+ private
45
+
113
46
  def pretty_file(file)
114
47
  <<~FILE
115
48
  #{file.path}
@@ -121,187 +54,115 @@ module Git
121
54
  FILE
122
55
  end
123
56
 
124
- # enumerable method
125
-
126
- def [](file)
127
- @files[file]
57
+ def select_files(&block)
58
+ @files.select { |_path, file| block.call(file) }
128
59
  end
129
60
 
130
- def each(&block)
131
- @files.values.each(&block)
61
+ def file_in_collection?(collection_name, file_path)
62
+ collection = public_send(collection_name)
63
+ if ignore_case?
64
+ downcased_keys(collection_name).include?(file_path.downcase)
65
+ else
66
+ collection.key?(file_path)
67
+ end
132
68
  end
133
69
 
134
- # subclass that does heavy lifting
135
- class StatusFile
136
- # @!attribute [r] path
137
- # The path of the file relative to the project root directory
138
- # @return [String]
139
- attr_accessor :path
140
-
141
- # @!attribute [r] type
142
- # The type of change
143
- #
144
- # * 'M': modified
145
- # * 'A': added
146
- # * 'D': deleted
147
- # * nil: ???
148
- #
149
- # @return [String]
150
- attr_accessor :type
151
-
152
- # @!attribute [r] mode_index
153
- # The mode of the file in the index
154
- # @return [String]
155
- # @example 100644
156
- #
157
- attr_accessor :mode_index
158
-
159
- # @!attribute [r] mode_repo
160
- # The mode of the file in the repo
161
- # @return [String]
162
- # @example 100644
163
- #
164
- attr_accessor :mode_repo
165
-
166
- # @!attribute [r] sha_index
167
- # The sha of the file in the index
168
- # @return [String]
169
- # @example 123456
170
- #
171
- attr_accessor :sha_index
70
+ def downcased_keys(collection_name)
71
+ @_downcased_keys ||= {}
72
+ @_downcased_keys[collection_name] ||=
73
+ public_send(collection_name).keys.to_set(&:downcase)
74
+ end
172
75
 
173
- # @!attribute [r] sha_repo
174
- # The sha of the file in the repo
175
- # @return [String]
176
- # @example 123456
177
- attr_accessor :sha_repo
76
+ def ignore_case?
77
+ return @_ignore_case if defined?(@_ignore_case)
178
78
 
179
- # @!attribute [r] untracked
180
- # Whether the file is untracked
181
- # @return [Boolean]
182
- attr_accessor :untracked
79
+ @_ignore_case = (@base.config('core.ignoreCase') == 'true')
80
+ rescue Git::FailedError
81
+ @_ignore_case = false
82
+ end
83
+ end
84
+ end
183
85
 
184
- # @!attribute [r] stage
185
- # The stage of the file
186
- #
187
- # * '0': the unmerged state
188
- # * '1': the common ancestor (or original) version
189
- # * '2': "our version" from the current branch head
190
- # * '3': "their version" from the other branch head
191
- # @return [String]
192
- attr_accessor :stage
86
+ module Git
87
+ class Status
88
+ # Represents a single file's status in the git repository. Each instance
89
+ # holds information about a file's state in the index and working tree.
90
+ class StatusFile
91
+ attr_reader :path, :type, :stage, :mode_index, :mode_repo,
92
+ :sha_index, :sha_repo, :untracked
193
93
 
194
94
  def initialize(base, hash)
195
- @base = base
196
- @path = hash[:path]
197
- @type = hash[:type]
198
- @stage = hash[:stage]
95
+ @base = base
96
+ @path = hash[:path]
97
+ @type = hash[:type]
98
+ @stage = hash[:stage]
199
99
  @mode_index = hash[:mode_index]
200
- @mode_repo = hash[:mode_repo]
201
- @sha_index = hash[:sha_index]
202
- @sha_repo = hash[:sha_repo]
203
- @untracked = hash[:untracked]
100
+ @mode_repo = hash[:mode_repo]
101
+ @sha_index = hash[:sha_index]
102
+ @sha_repo = hash[:sha_repo]
103
+ @untracked = hash[:untracked]
204
104
  end
205
105
 
106
+ # Returns a Git::Object::Blob for either the index or repo version of the file.
206
107
  def blob(type = :index)
207
- if type == :repo
208
- @base.object(@sha_repo)
209
- else
210
- begin
211
- @base.object(@sha_index)
212
- rescue
213
- @base.object(@sha_repo)
214
- end
215
- end
108
+ sha = type == :repo ? sha_repo : (sha_index || sha_repo)
109
+ @base.object(sha) if sha
216
110
  end
217
111
  end
112
+ end
113
+ end
218
114
 
219
- private
220
-
221
- def construct_status
222
- # Lists all files in the index and the worktree
223
- # git ls-files --stage
224
- # { file => { path: file, mode_index: '100644', sha_index: 'dd4fc23', stage: '0' } }
225
- @files = @base.lib.ls_files
226
-
227
- # Lists files in the worktree that are not in the index
228
- # Add untracked files to @files
229
- fetch_untracked
230
-
231
- # Lists files that are different between the index vs. the worktree
232
- fetch_modified
233
-
234
- # Lists files that are different between the repo HEAD vs. the worktree
235
- fetch_added
236
-
237
- @files.each do |k, file_hash|
238
- @files[k] = StatusFile.new(@base, file_hash)
115
+ module Git
116
+ class Status
117
+ # A factory class responsible for fetching git status data and building
118
+ # a hash of StatusFile objects.
119
+ # @api private
120
+ class StatusFileFactory
121
+ def initialize(base)
122
+ @base = base
123
+ @lib = base.lib
239
124
  end
240
- end
241
125
 
242
- def fetch_untracked
243
- # git ls-files --others --exclude-standard, chdir: @git_work_dir)
244
- # { file => { path: file, untracked: true } }
245
- @base.lib.untracked_files.each do |file|
246
- @files[file] = { path: file, untracked: true }
126
+ # Gathers all status data and builds a hash of file paths to
127
+ # StatusFile objects.
128
+ def construct_files
129
+ files_data = fetch_all_files_data
130
+ files_data.transform_values do |data|
131
+ StatusFile.new(@base, data)
132
+ end
247
133
  end
248
- end
249
134
 
250
- def fetch_modified
251
- # Files changed between the index vs. the worktree
252
- # git diff-files
253
- # { file => { path: file, type: 'M', mode_index: '100644', mode_repo: '100644', sha_index: '0000000', :sha_repo: '52c6c4e' } }
254
- @base.lib.diff_files.each do |path, data|
255
- @files[path] ? @files[path].merge!(data) : @files[path] = data
135
+ private
136
+
137
+ # Fetches and merges status information from multiple git commands.
138
+ def fetch_all_files_data
139
+ files = @lib.ls_files # Start with files tracked in the index.
140
+ merge_untracked_files(files)
141
+ merge_modified_files(files)
142
+ merge_head_diffs(files)
143
+ files
256
144
  end
257
- end
258
145
 
259
- def fetch_added
260
- unless @base.lib.empty?
261
- # Files changed between the repo HEAD vs. the worktree
262
- # git diff-index HEAD
263
- # { file => { path: file, type: 'M', mode_index: '100644', mode_repo: '100644', sha_index: '0000000', :sha_repo: '52c6c4e' } }
264
- @base.lib.diff_index('HEAD').each do |path, data|
265
- @files[path] ? @files[path].merge!(data) : @files[path] = data
146
+ def merge_untracked_files(files)
147
+ @lib.untracked_files.each do |file|
148
+ files[file] = { path: file, untracked: true }
266
149
  end
267
150
  end
268
- end
269
151
 
270
- # It's worth noting that (like git itself) this gem will not behave well if
271
- # ignoreCase is set inconsistently with the file-system itself. For details:
272
- # https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreignoreCase
273
- def ignore_case?
274
- return @_ignore_case if defined?(@_ignore_case)
275
- @_ignore_case = @base.config('core.ignoreCase') == 'true'
276
- rescue Git::FailedError
277
- @_ignore_case = false
278
- end
279
-
280
- def downcase_keys(hash)
281
- hash.map { |k, v| [k.downcase, v] }.to_h
282
- end
283
-
284
- def lc_changed
285
- @_lc_changed ||= changed.transform_keys(&:downcase)
286
- end
287
-
288
- def lc_added
289
- @_lc_added ||= added.transform_keys(&:downcase)
290
- end
152
+ def merge_modified_files(files)
153
+ # Merge changes between the index and the working directory.
154
+ @lib.diff_files.each do |path, data|
155
+ (files[path] ||= {}).merge!(data)
156
+ end
157
+ end
291
158
 
292
- def lc_deleted
293
- @_lc_deleted ||= deleted.transform_keys(&:downcase)
294
- end
159
+ def merge_head_diffs(files)
160
+ return if @lib.empty?
295
161
 
296
- def lc_untracked
297
- @_lc_untracked ||= untracked.transform_keys(&:downcase)
298
- end
299
-
300
- def case_aware_include?(cased_hash, downcased_hash, file)
301
- if ignore_case?
302
- send(downcased_hash).include?(file.downcase)
303
- else
304
- send(cased_hash).include?(file)
162
+ # Merge changes between HEAD and the index.
163
+ @lib.diff_index('HEAD').each do |path, data|
164
+ (files[path] ||= {}).merge!(data)
165
+ end
305
166
  end
306
167
  end
307
168
  end
data/lib/git/url.rb CHANGED
@@ -23,7 +23,7 @@ module Git
23
23
  :(?!/) # : serparator is required, but must not be followed by /
24
24
  (?<path>.*?) # path is required
25
25
  $
26
- }x.freeze
26
+ }x
27
27
 
28
28
  # Parse a Git URL and return an Addressable::URI object
29
29
  #
@@ -118,9 +118,9 @@ module Git
118
118
  #
119
119
  def to_s
120
120
  if user
121
- "#{user}@#{host}:#{path[1..-1]}"
121
+ "#{user}@#{host}:#{path[1..]}"
122
122
  else
123
- "#{host}:#{path[1..-1]}"
123
+ "#{host}:#{path[1..]}"
124
124
  end
125
125
  end
126
126
  end
data/lib/git/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  module Git
4
4
  # The current gem version
5
5
  # @return [String] the current gem version.
6
- VERSION='4.0.0'
6
+ VERSION = '4.0.1'
7
7
  end
data/lib/git/worktree.rb CHANGED
@@ -3,14 +3,13 @@
3
3
  require 'git/path'
4
4
 
5
5
  module Git
6
-
7
- class Worktree < Path
8
-
9
- attr_accessor :full, :dir, :gcommit
6
+ # A worktree in a Git repository
7
+ class Worktree
8
+ attr_accessor :full, :dir
10
9
 
11
10
  def initialize(base, dir, gcommit = nil)
12
11
  @full = dir
13
- @full += ' ' + gcommit if !gcommit.nil?
12
+ @full += " #{gcommit}" unless gcommit.nil?
14
13
  @base = base
15
14
  @dir = dir
16
15
  @gcommit = gcommit
data/lib/git/worktrees.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  module Git
4
4
  # object that holds all the available worktrees
5
5
  class Worktrees
6
-
7
6
  include Enumerable
8
7
 
9
8
  def initialize(base)
@@ -23,20 +22,19 @@ module Git
23
22
  @worktrees.size
24
23
  end
25
24
 
26
- def each(&block)
27
- @worktrees.values.each(&block)
25
+ def each(&)
26
+ @worktrees.values.each(&)
28
27
  end
29
28
 
30
29
  def [](worktree_name)
31
- @worktrees.values.inject(@worktrees) do |worktrees, worktree|
30
+ @worktrees.values.each_with_object(@worktrees) do |worktree, worktrees|
32
31
  worktrees[worktree.full] ||= worktree
33
- worktrees
34
32
  end[worktree_name.to_s]
35
33
  end
36
34
 
37
35
  def to_s
38
36
  out = ''
39
- @worktrees.each do |k, b|
37
+ @worktrees.each_value do |b|
40
38
  out << b.to_s << "\n"
41
39
  end
42
40
  out
data/lib/git.rb CHANGED
@@ -42,16 +42,16 @@ require 'git/worktrees'
42
42
  # @author Scott Chacon (mailto:schacon@gmail.com)
43
43
  #
44
44
  module Git
45
- #g.config('user.name', 'Scott Chacon') # sets value
46
- #g.config('user.email', 'email@email.com') # sets value
47
- #g.config('user.name') # returns 'Scott Chacon'
48
- #g.config # returns whole config hash
45
+ # g.config('user.name', 'Scott Chacon') # sets value
46
+ # g.config('user.email', 'email@email.com') # sets value
47
+ # g.config('user.name') # returns 'Scott Chacon'
48
+ # g.config # returns whole config hash
49
49
  def config(name = nil, value = nil)
50
50
  lib = Git::Lib.new
51
- if(name && value)
51
+ if name && value
52
52
  # set value
53
53
  lib.config_set(name, value)
54
- elsif (name)
54
+ elsif name
55
55
  # return value
56
56
  lib.config_get(name)
57
57
  else
@@ -191,7 +191,7 @@ module Git
191
191
  # of the cloned local working copy or cloned repository.
192
192
  #
193
193
  def self.clone(repository_url, directory = nil, options = {})
194
- clone_to_options = options.select { |key, _value| %i[bare mirror].include?(key) }
194
+ clone_to_options = options.slice(:bare, :mirror)
195
195
  directory ||= Git::URL.clone_to(repository_url, **clone_to_options)
196
196
  Base.clone(repository_url, directory, options)
197
197
  end
@@ -216,7 +216,8 @@ module Git
216
216
  # @example with the logging option
217
217
  # logger = Logger.new(STDOUT, level: Logger::INFO)
218
218
  # Git.default_branch('.', log: logger) # => 'master'
219
- # I, [2022-04-13T16:01:33.221596 #18415] INFO -- : git '-c' 'core.quotePath=true' '-c' 'color.ui=false' ls-remote '--symref' '--' '.' 'HEAD' 2>&1
219
+ # I, [2022-04-13T16:01:33.221596 #18415] INFO -- : git '-c' 'core.quotePath=true'
220
+ # '-c' 'color.ui=false' ls-remote '--symref' '--' '.' 'HEAD' 2>&1
220
221
  #
221
222
  # @param repository [URI, Pathname, String] The (possibly remote) repository to get the default branch name for
222
223
  #
@@ -245,23 +246,23 @@ module Git
245
246
  # remote, 'origin.'
246
247
  def self.export(repository, name, options = {})
247
248
  options.delete(:remote)
248
- repo = clone(repository, name, {:depth => 1}.merge(options))
249
+ repo = clone(repository, name, { depth: 1 }.merge(options))
249
250
  repo.checkout("origin/#{options[:branch]}") if options[:branch]
250
251
  FileUtils.rm_r File.join(repo.dir.to_s, '.git')
251
252
  end
252
253
 
253
254
  # Same as g.config, but forces it to be at the global level
254
255
  #
255
- #g.config('user.name', 'Scott Chacon') # sets value
256
- #g.config('user.email', 'email@email.com') # sets value
257
- #g.config('user.name') # returns 'Scott Chacon'
258
- #g.config # returns whole config hash
256
+ # g.config('user.name', 'Scott Chacon') # sets value
257
+ # g.config('user.email', 'email@email.com') # sets value
258
+ # g.config('user.name') # returns 'Scott Chacon'
259
+ # g.config # returns whole config hash
259
260
  def self.global_config(name = nil, value = nil)
260
261
  lib = Git::Lib.new(nil, nil)
261
- if(name && value)
262
+ if name && value
262
263
  # set value
263
264
  lib.global_config_set(name, value)
264
- elsif (name)
265
+ elsif name
265
266
  # return value
266
267
  lib.global_config_get(name)
267
268
  else