git 5.2.0 → 5.4.0

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/stashes.rb CHANGED
@@ -3,12 +3,23 @@
3
3
  module Git
4
4
  # Collection of stash entries for a Git repository
5
5
  #
6
- # @example Iterate over stash entries
6
+ # This class is deprecated and will be removed in v6.0.0. Use the
7
+ # {Git::Repository} stash methods and {Git::StashInfo} instead:
8
+ # {Git::Repository#stash_infos} replaces the collection, and
9
+ # {Git::Repository#stash_push}, {Git::Repository#stash_apply}, and
10
+ # {Git::Repository#stash_clear} replace {#save}, {#apply}, and {#clear}.
11
+ #
12
+ # @example Iterate over stash entries (deprecated)
7
13
  # git.stashes.each { |s| puts s.message }
8
14
  #
9
- # @example Check and apply a stash
10
- # git.stashes.size #=> 2
11
- # git.stashes.apply
15
+ # @example The replacement
16
+ # repo.stash_infos.each { |info| puts info.message }
17
+ # repo.stash_infos.size #=> 2
18
+ # repo.stash_apply
19
+ #
20
+ # @deprecated Use {Git::Repository#stash_infos} and {Git::StashInfo} instead
21
+ #
22
+ # @see Git::Repository#stash_infos
12
23
  #
13
24
  # @api public
14
25
  #
@@ -18,6 +29,7 @@ module Git
18
29
  # Initialize the stashes collection
19
30
  #
20
31
  # Loads all existing stash entries from the repository at construction time.
32
+ # Emits one deprecation warning per object.
21
33
  #
22
34
  # @example Load stashes for a repository
23
35
  # stashes = Git::Stashes.new(repo)
@@ -28,14 +40,20 @@ module Git
28
40
  # @return [void]
29
41
  #
30
42
  # @raise [Git::FailedError] if git exits with a non-zero exit status
43
+ #
44
+ # @deprecated Use {Git::Repository#stash_infos} and {Git::StashInfo} instead
45
+ #
31
46
  def initialize(base)
47
+ Git::Deprecation.warn(
48
+ 'Git::Stashes is deprecated and will be removed in v6.0.0. ' \
49
+ 'Use the Git::Repository stash methods (stash_infos, stash_push, stash_apply, stash_clear) ' \
50
+ 'and Git::StashInfo instead.'
51
+ )
32
52
  @stashes = []
33
53
  @base = base
34
-
35
- stash_repository.stashes_all.each do |stash|
36
- message = stash[1]
37
- @stashes.unshift(Git::Stash.new(@base, message, existing: true))
38
- end
54
+ # stashes_all and Git::Stash are deprecated too; silence them so one
55
+ # Git::Stashes.new emits one warning
56
+ Git::Deprecation.silence { load_stashes }
39
57
  end
40
58
 
41
59
  # Returns all stash entries as an array of index and message pairs
@@ -51,7 +69,8 @@ module Git
51
69
  # @raise [Git::FailedError] if git exits with a non-zero exit status
52
70
  #
53
71
  def all
54
- stash_repository.stashes_all
72
+ # stashes_all is deprecated too; silence it so this call emits no second warning
73
+ Git::Deprecation.silence { stash_repository.stashes_all }
55
74
  end
56
75
 
57
76
  # Saves the current working-directory state to a new stash entry
@@ -65,8 +84,10 @@ module Git
65
84
  # @return [void]
66
85
  #
67
86
  # @raise [Git::FailedError] if git exits with a non-zero exit status
87
+ #
68
88
  def save(message)
69
- s = Git::Stash.new(@base, message)
89
+ # Git::Stash is deprecated too; silence it so this call emits no second warning
90
+ s = Git::Deprecation.silence { Git::Stash.new(@base, message) }
70
91
  @stashes.unshift(s) if s.saved?
71
92
  end
72
93
 
@@ -83,6 +104,7 @@ module Git
83
104
  # @return [String] the output from the git stash apply command
84
105
  #
85
106
  # @raise [Git::FailedError] if git exits with a non-zero exit status
107
+ #
86
108
  def apply(index = nil)
87
109
  stash_repository.stash_apply(index)
88
110
  end
@@ -96,6 +118,7 @@ module Git
96
118
  # @return [void]
97
119
  #
98
120
  # @raise [Git::FailedError] if git exits with a non-zero exit status
121
+ #
99
122
  def clear
100
123
  stash_repository.stash_clear
101
124
  @stashes = []
@@ -108,6 +131,7 @@ module Git
108
131
  # git.stashes.size #=> 2
109
132
  #
110
133
  # @return [Integer] the number of stashes
134
+ #
111
135
  def size
112
136
  @stashes.size
113
137
  end
@@ -145,12 +169,24 @@ module Git
145
169
  # @param index [Integer, #to_i] the stash index (0 = most recent)
146
170
  #
147
171
  # @return [Git::Stash, nil] the stash entry, or `nil` if the index is out of bounds
172
+ #
148
173
  def [](index)
149
174
  @stashes[index.to_i]
150
175
  end
151
176
 
152
177
  private
153
178
 
179
+ # Wraps every entry from the repository in a Git::Stash, newest first
180
+ #
181
+ # @return [void]
182
+ #
183
+ def load_stashes
184
+ stash_repository.stashes_all.each do |stash|
185
+ message = stash[1]
186
+ @stashes.unshift(Git::Stash.new(@base, message, existing: true))
187
+ end
188
+ end
189
+
154
190
  # Returns the facade interface for stash operations
155
191
  #
156
192
  # @return [Git::Repository]
data/lib/git/status.rb CHANGED
@@ -13,6 +13,9 @@ module Git
13
13
  # status.deleted.each { |path, _file| puts "Deleted: #{path}" }
14
14
  # status.untracked.each { |path, _file| puts "Untracked: #{path}" }
15
15
  #
16
+ # @deprecated Use {Git::StatusInfo}, returned by {Git::Repository#status_info},
17
+ # instead; this class will be removed in v6.0.0
18
+ #
16
19
  # @api public
17
20
  #
18
21
  class Status
@@ -22,7 +25,15 @@ module Git
22
25
  #
23
26
  # @param base [Git::Repository] the git object backing this status
24
27
  #
28
+ # @deprecated Use {Git::Repository#status_info} instead
29
+ #
25
30
  def initialize(base)
31
+ if defined?(Git::Deprecation)
32
+ Git::Deprecation.warn(
33
+ 'Git::Status is deprecated and will be removed in v6.0.0. ' \
34
+ 'Use Git::Repository#status_info instead.'
35
+ )
36
+ end
26
37
  @base = base
27
38
  # The factory returns a hash of file paths to StatusFile objects.
28
39
  @files = StatusFileFactory.new(base).construct_files
@@ -204,6 +215,9 @@ module Git
204
215
  # Represents a single file's status in the git repository. Each instance
205
216
  # holds information about a file's state in the index and working tree.
206
217
  #
218
+ # @deprecated Use {Git::StatusFileInfo}, held by {Git::StatusInfo#files},
219
+ # instead; this class will be removed in v6.0.0
220
+ #
207
221
  # @api public
208
222
  #
209
223
  class StatusFile
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Immutable value object for the status of one path in the index and working tree
5
+ #
6
+ # Each member holds one field of the entry `git status --porcelain=v2` reports
7
+ # for the path. `index_status` and `worktree_status` are the `X` and `Y`
8
+ # characters of that entry: `.` unmodified, `M` modified, `T` type changed, `A`
9
+ # added, `D` deleted, `R` renamed, `C` copied, or `U` unmerged. Untracked
10
+ # entries carry `?` in both and ignored entries carry `!` in both, mirroring
11
+ # the `??` and `!!` codes of the short status format.
12
+ #
13
+ # The mode and SHA members are `nil` for untracked and ignored entries. For
14
+ # unmerged entries the per-stage modes and SHAs live in `unmerged_stages` and
15
+ # `mode_head`, `mode_index`, `sha_head`, and `sha_index` are `nil`.
16
+ # `original_path` and `rename_score` are set only for rename and copy entries.
17
+ #
18
+ # The predicates follow these rules: `added?` when `index_status` is `A`,
19
+ # `deleted?` when either status is `D`, `changed?` when either status is `M`
20
+ # or `T`, `renamed?` when either status is `R`, `untracked?` and `ignored?`
21
+ # from the `?` and `!` codes, and `unmerged?` when `unmerged_stages` is set.
22
+ #
23
+ # Every String member is a frozen copy of the value given, and
24
+ # `unmerged_stages` is a deeply frozen copy, so an entry cannot be changed
25
+ # through a member the caller still references.
26
+ #
27
+ # @example Inspect the status of one path
28
+ # file = repo.status_info['lib/foo.rb']
29
+ # file.index_status #=> "M"
30
+ # file.worktree_status #=> "."
31
+ # file.changed? #=> true
32
+ # file.sha_index #=> "2bdf67abb163a4ffb2d7f3f0880c9fe5068ce782"
33
+ #
34
+ # @example Read the original path of a rename
35
+ # file = repo.status_info['lib/new_name.rb']
36
+ # file.renamed? #=> true
37
+ # file.original_path #=> "lib/old_name.rb"
38
+ # file.rename_score #=> 100
39
+ #
40
+ # @example Read the stages of a merge conflict
41
+ # file = repo.status_info['lib/conflict.rb']
42
+ # file.unmerged? #=> true
43
+ # file.unmerged_stages[2] #=> { mode: "100644", sha: "ba2906d0666c..." }
44
+ #
45
+ # @see Git::StatusInfo
46
+ #
47
+ # @see Git::Repository#status_info
48
+ #
49
+ # @see https://git-scm.com/docs/git-status#_porcelain_format_version_2
50
+ #
51
+ # @api public
52
+ #
53
+ # @!attribute [r] path
54
+ #
55
+ # @return [String] the repository-relative path
56
+ #
57
+ # @!attribute [r] index_status
58
+ #
59
+ # @return [String] the `X` status character (HEAD versus index), `?` for
60
+ # untracked and `!` for ignored entries
61
+ #
62
+ # @!attribute [r] worktree_status
63
+ #
64
+ # @return [String] the `Y` status character (index versus working tree), `?`
65
+ # for untracked and `!` for ignored entries
66
+ #
67
+ # @!attribute [r] submodule
68
+ #
69
+ # @return [String, nil] the four-character submodule state (`N...` for a
70
+ # regular file), or `nil` for untracked and ignored entries
71
+ #
72
+ # @!attribute [r] mode_head
73
+ #
74
+ # @return [String, nil] the octal file mode in HEAD (`000000` when the path
75
+ # is not in HEAD), or `nil` for untracked, ignored, and unmerged entries
76
+ #
77
+ # @!attribute [r] mode_index
78
+ #
79
+ # @return [String, nil] the octal file mode in the index (`000000` when the
80
+ # path is not in the index), or `nil` for untracked, ignored, and unmerged
81
+ # entries
82
+ #
83
+ # @!attribute [r] mode_worktree
84
+ #
85
+ # @return [String, nil] the octal file mode in the working tree (`000000`
86
+ # when the path is not in the working tree), or `nil` for untracked and
87
+ # ignored entries
88
+ #
89
+ # @!attribute [r] sha_head
90
+ #
91
+ # @return [String, nil] the object name of the blob in HEAD (all zeros when
92
+ # the path is not in HEAD), or `nil` for untracked, ignored, and unmerged
93
+ # entries
94
+ #
95
+ # @!attribute [r] sha_index
96
+ #
97
+ # @return [String, nil] the object name of the blob in the index (all zeros
98
+ # when the path is not in the index), or `nil` for untracked, ignored, and
99
+ # unmerged entries
100
+ #
101
+ # @!attribute [r] original_path
102
+ #
103
+ # @return [String, nil] the path the entry was renamed or copied from, or
104
+ # `nil` for every other entry
105
+ #
106
+ # @!attribute [r] rename_score
107
+ #
108
+ # @return [Integer, nil] the similarity score of a rename or copy entry, or
109
+ # `nil` for every other entry
110
+ #
111
+ # @!attribute [r] unmerged_stages
112
+ #
113
+ # @return [Hash{Integer => Hash{Symbol => String}}, nil] the mode and SHA of
114
+ # each conflict stage keyed by stage number (1 for the merge base, 2 for
115
+ # "ours", 3 for "theirs"), as frozen `\\{ mode:, sha: }` hashes, or `nil`
116
+ # for every other entry
117
+ #
118
+ StatusFileInfo = Data.define(
119
+ :path,
120
+ :index_status,
121
+ :worktree_status,
122
+ :submodule,
123
+ :mode_head,
124
+ :mode_index,
125
+ :mode_worktree,
126
+ :sha_head,
127
+ :sha_index,
128
+ :original_path,
129
+ :rename_score,
130
+ :unmerged_stages
131
+ ) do
132
+ # Creates a file status value object holding frozen copies of its members
133
+ #
134
+ # String members are duplicated and frozen, and `unmerged_stages` is
135
+ # duplicated and frozen down to its mode and SHA strings, so neither this
136
+ # value nor a {Git::StatusInfo} holding it can be changed through a member
137
+ # the caller still references.
138
+ #
139
+ # @example Build an entry from parsed fields
140
+ # Git::StatusFileInfo.new(path: 'lib/foo.rb', index_status: 'M', worktree_status: '.', ...)
141
+ #
142
+ # @param members [Hash{Symbol => Object}] one value per member; every
143
+ # member is required
144
+ #
145
+ # @option members [String] :path the repository-relative path
146
+ #
147
+ # @option members [String] :index_status the `X` status character
148
+ #
149
+ # @option members [String] :worktree_status the `Y` status character
150
+ #
151
+ # @option members [String, nil] :submodule the four-character submodule state
152
+ #
153
+ # @option members [String, nil] :mode_head the octal file mode in HEAD
154
+ #
155
+ # @option members [String, nil] :mode_index the octal file mode in the index
156
+ #
157
+ # @option members [String, nil] :mode_worktree the octal file mode in the working tree
158
+ #
159
+ # @option members [String, nil] :sha_head the object name of the blob in HEAD
160
+ #
161
+ # @option members [String, nil] :sha_index the object name of the blob in the index
162
+ #
163
+ # @option members [String, nil] :original_path the path a rename or copy came from
164
+ #
165
+ # @option members [Integer, nil] :rename_score the similarity score of a rename or copy
166
+ #
167
+ # @option members [Hash{Integer => Hash{Symbol => String}}, nil] :unmerged_stages
168
+ # the mode and SHA of each conflict stage keyed by stage number
169
+ #
170
+ def initialize(**members)
171
+ super(**members.transform_values { |value| deep_frozen(value) })
172
+ end
173
+
174
+ # Returns `true` when the path is not tracked by git
175
+ #
176
+ # @example Check an untracked path
177
+ # repo.status_info['new.rb'].untracked? #=> true
178
+ #
179
+ # @return [Boolean] `true` when both status characters are `?`
180
+ #
181
+ def untracked? = index_status == '?' && worktree_status == '?'
182
+
183
+ # Returns `true` when the path is ignored
184
+ #
185
+ # Ignored entries are reported only when `git status` runs with `--ignored`.
186
+ # {Git::Repository#status_info} does not pass that option, so this is
187
+ # `false` for every entry it returns.
188
+ #
189
+ # @example Check an ignored entry
190
+ # file = Git::Parsers::Status.parse("! tmp/debug.log\0").first
191
+ # file.ignored? #=> true
192
+ #
193
+ # @return [Boolean] `true` when both status characters are `!`
194
+ #
195
+ def ignored? = index_status == '!' && worktree_status == '!'
196
+
197
+ # Returns `true` when the path has merge conflicts
198
+ #
199
+ # @example Check a conflicted path
200
+ # repo.status_info['lib/conflict.rb'].unmerged? #=> true
201
+ #
202
+ # @return [Boolean] `true` when `unmerged_stages` is set
203
+ #
204
+ def unmerged? = !unmerged_stages.nil?
205
+
206
+ # Returns `true` when the path was renamed in the index or working tree
207
+ #
208
+ # @example Check a renamed path
209
+ # repo.status_info['lib/new_name.rb'].renamed? #=> true
210
+ #
211
+ # @return [Boolean] `true` when either status character is `R`
212
+ #
213
+ def renamed? = index_status == 'R' || worktree_status == 'R'
214
+
215
+ # Returns `true` when the path was added to the index and is not in HEAD
216
+ #
217
+ # @example Check a newly staged path
218
+ # repo.status_info['lib/new.rb'].added? #=> true
219
+ #
220
+ # @return [Boolean] `true` when `index_status` is `A`
221
+ #
222
+ def added? = index_status == 'A'
223
+
224
+ # Returns `true` when the path was deleted from the index or working tree
225
+ #
226
+ # @example Check a deleted path
227
+ # repo.status_info['lib/old.rb'].deleted? #=> true
228
+ #
229
+ # @return [Boolean] `true` when either status character is `D`
230
+ #
231
+ def deleted? = index_status == 'D' || worktree_status == 'D'
232
+
233
+ # Returns `true` when the path's content or type changed in the index or working tree
234
+ #
235
+ # @example Check a modified path
236
+ # repo.status_info['lib/foo.rb'].changed? #=> true
237
+ #
238
+ # @return [Boolean] `true` when either status character is `M` or `T`
239
+ #
240
+ def changed? = [index_status, worktree_status].intersect?(%w[M T])
241
+
242
+ private
243
+
244
+ # Returns a frozen copy of `value`, freezing the contents of a Hash recursively
245
+ #
246
+ # @param value [Object] a member value
247
+ #
248
+ # @return [Object] a frozen copy of a String or Hash; any other value as given
249
+ #
250
+ def deep_frozen(value)
251
+ case value
252
+ when String then value.dup.freeze
253
+ when Hash then value.to_h { |key, item| [key, deep_frozen(item)] }.freeze
254
+ else value
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/status_file_info'
4
+
5
+ module Git
6
+ # Immutable value object for the status of a repository's index and working tree
7
+ #
8
+ # Holds one {Git::StatusFileInfo} per path that `git status` reports, in the
9
+ # order git listed them, together with the repository's `core.ignoreCase`
10
+ # setting. The derived readers (`changed`, `added`, `deleted`, `untracked`,
11
+ # `unmerged`) return the matching files keyed by path and are computed on each
12
+ # call. The path predicates (`changed?`, `added?`, `deleted?`, `untracked?`)
13
+ # compare paths case-insensitively when `ignore_case` is `true`.
14
+ #
15
+ # @example Inspect repository status
16
+ # status = repo.status_info
17
+ # status.changed.each_key { |path| puts "Modified: #{path}" }
18
+ # status.added.each_key { |path| puts "Added: #{path}" }
19
+ # status.deleted.each_key { |path| puts "Deleted: #{path}" }
20
+ # status.untracked.each_key { |path| puts "Untracked: #{path}" }
21
+ #
22
+ # @example Check one path
23
+ # status = repo.status_info
24
+ # status.changed?('lib/foo.rb') #=> true
25
+ # status['lib/foo.rb'].worktree_status #=> "M"
26
+ #
27
+ # @see Git::StatusFileInfo
28
+ #
29
+ # @see Git::Repository#status_info
30
+ #
31
+ # @api public
32
+ #
33
+ # @!attribute [r] files
34
+ #
35
+ # @return [Array<Git::StatusFileInfo>] every reported path in git's output
36
+ # order; the array is frozen
37
+ #
38
+ # @!attribute [r] ignore_case
39
+ #
40
+ # @return [Boolean] `true` when the repository's `core.ignoreCase` is true,
41
+ # making the path predicates compare paths case-insensitively
42
+ #
43
+ StatusInfo = Data.define(:files, :ignore_case) do
44
+ # Creates a status value object holding a frozen copy of the given files
45
+ #
46
+ # @example Build a status from parsed files
47
+ # Git::StatusInfo.new(files: files, ignore_case: false)
48
+ #
49
+ # @param files [Array<Git::StatusFileInfo>] the reported paths in git's
50
+ # output order
51
+ #
52
+ # @param ignore_case [Boolean] whether path predicates ignore case
53
+ #
54
+ def initialize(files:, ignore_case:)
55
+ super(files: files.dup.freeze, ignore_case: ignore_case)
56
+ end
57
+
58
+ # Returns the files modified or type-changed in the index or working tree
59
+ #
60
+ # @example List modified paths
61
+ # repo.status_info.changed.keys #=> ["lib/foo.rb"]
62
+ #
63
+ # @return [Hash{String => Git::StatusFileInfo}] changed files keyed by path
64
+ #
65
+ def changed = files_by_path(&:changed?)
66
+
67
+ # Returns the files added to the index that are not in HEAD
68
+ #
69
+ # @example List added paths
70
+ # repo.status_info.added.keys #=> ["lib/new.rb"]
71
+ #
72
+ # @return [Hash{String => Git::StatusFileInfo}] added files keyed by path
73
+ #
74
+ def added = files_by_path(&:added?)
75
+
76
+ # Returns the files deleted from the index or working tree
77
+ #
78
+ # @example List deleted paths
79
+ # repo.status_info.deleted.keys #=> ["lib/old.rb"]
80
+ #
81
+ # @return [Hash{String => Git::StatusFileInfo}] deleted files keyed by path
82
+ #
83
+ def deleted = files_by_path(&:deleted?)
84
+
85
+ # Returns the files in the working tree that git does not track
86
+ #
87
+ # @example List untracked paths
88
+ # repo.status_info.untracked.keys #=> ["notes.txt"]
89
+ #
90
+ # @return [Hash{String => Git::StatusFileInfo}] untracked files keyed by path
91
+ #
92
+ def untracked = files_by_path(&:untracked?)
93
+
94
+ # Returns the files with merge conflicts
95
+ #
96
+ # @example List conflicted paths
97
+ # repo.status_info.unmerged.keys #=> ["lib/conflict.rb"]
98
+ #
99
+ # @return [Hash{String => Git::StatusFileInfo}] unmerged files keyed by path
100
+ #
101
+ def unmerged = files_by_path(&:unmerged?)
102
+
103
+ # Returns `true` if `path` is modified in the index or working tree
104
+ #
105
+ # @example Check a path
106
+ # repo.status_info.changed?('lib/foo.rb') #=> true
107
+ #
108
+ # @param path [String] the repository-relative path to check
109
+ #
110
+ # @return [Boolean] `true` when the path is in {#changed}
111
+ #
112
+ def changed?(path) = path_in?(changed, path)
113
+
114
+ # Returns `true` if `path` was added to the index
115
+ #
116
+ # @example Check a path
117
+ # repo.status_info.added?('lib/new.rb') #=> true
118
+ #
119
+ # @param path [String] the repository-relative path to check
120
+ #
121
+ # @return [Boolean] `true` when the path is in {#added}
122
+ #
123
+ def added?(path) = path_in?(added, path)
124
+
125
+ # Returns `true` if `path` was deleted from the index or working tree
126
+ #
127
+ # @example Check a path
128
+ # repo.status_info.deleted?('lib/old.rb') #=> true
129
+ #
130
+ # @param path [String] the repository-relative path to check
131
+ #
132
+ # @return [Boolean] `true` when the path is in {#deleted}
133
+ #
134
+ def deleted?(path) = path_in?(deleted, path)
135
+
136
+ # Returns `true` if `path` is not tracked by git
137
+ #
138
+ # @example Check a path
139
+ # repo.status_info.untracked?('notes.txt') #=> true
140
+ #
141
+ # @param path [String] the repository-relative path to check
142
+ #
143
+ # @return [Boolean] `true` when the path is in {#untracked}
144
+ #
145
+ def untracked?(path) = path_in?(untracked, path)
146
+
147
+ # Returns the {Git::StatusFileInfo} for the given path
148
+ #
149
+ # The path is matched exactly, regardless of `ignore_case`.
150
+ #
151
+ # @example Look up a path
152
+ # repo.status_info['lib/foo.rb'] #=> #<data Git::StatusFileInfo path="lib/foo.rb", ...>
153
+ # repo.status_info['clean.rb'] #=> nil
154
+ #
155
+ # @param path [String] the repository-relative path
156
+ #
157
+ # @return [Git::StatusFileInfo, nil] the file, or `nil` when git did not report it
158
+ #
159
+ def [](path) = files.find { |file| file.path == path }
160
+
161
+ private
162
+
163
+ # Returns the files for which the block is truthy, keyed by path
164
+ #
165
+ # @return [Hash{String => Git::StatusFileInfo}] the selected files keyed by path
166
+ #
167
+ # @yield [file] each {Git::StatusFileInfo} in `files`
168
+ #
169
+ # @yieldparam file [Git::StatusFileInfo] one reported path
170
+ #
171
+ # @yieldreturn [Boolean] truthy to include the file
172
+ #
173
+ def files_by_path(&) = files.select(&).to_h { |file| [file.path, file] }
174
+
175
+ # Returns `true` when `path` is a key of `collection`, honoring `ignore_case`
176
+ #
177
+ # @param collection [Hash{String => Git::StatusFileInfo}] files keyed by path
178
+ #
179
+ # @param path [String] the repository-relative path to look for
180
+ #
181
+ # @return [Boolean] `true` when the path is present
182
+ #
183
+ def path_in?(collection, path)
184
+ return collection.key?(path) unless ignore_case
185
+
186
+ collection.each_key.any? { |key| key.casecmp?(path) }
187
+ end
188
+ end
189
+ end