git 5.3.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.
@@ -2,16 +2,466 @@
2
2
 
3
3
  require 'git/commands/stash'
4
4
  require 'git/parsers/stash'
5
+ require 'git/repository/shared_private'
5
6
 
6
7
  module Git
7
8
  class Repository
8
9
  # Facade methods for stash operations
9
10
  #
11
+ # Each method maps onto a `git stash` subcommand. Methods that identify or
12
+ # create a stash entry ({#stash_infos}, {#stash_push}, {#stash_store}) return
13
+ # {Git::StashInfo} values; methods that only change or display the stash
14
+ # return git's stdout. Every method that takes a stash ({#stash_apply},
15
+ # {#stash_pop}, {#stash_drop}, {#stash_show}, {#stash_branch}) accepts a
16
+ # {Git::StashInfo}, a `stash@{N}` name, an Integer index (`0` is the most
17
+ # recent entry), or `nil` for the most recent entry. Methods that take
18
+ # options accept them as a trailing positional Hash, so
19
+ # `repo.stash_apply(index: true)` and `repo.stash_apply(nil, opts)` with a
20
+ # stored `opts` Hash both work.
21
+ #
22
+ # {#stashes_all}, {#stash_save}, and {#stash_list} are the legacy surface.
23
+ # They are deprecated and will be removed in v6.0.0.
24
+ #
10
25
  # Included by {Git::Repository}.
11
26
  #
12
27
  # @api private
13
28
  #
14
29
  module Stashing
30
+ # Returns every stash entry as a {Git::StashInfo}, newest first
31
+ #
32
+ # The order and indices match `git stash list`: the first element is
33
+ # `stash@{0}`, the most recent entry.
34
+ #
35
+ # @example List stash entries (newest first)
36
+ # repo.stash_infos.map(&:name) #=> ["stash@{0}", "stash@{1}"]
37
+ #
38
+ # @example Apply the oldest entry
39
+ # repo.stash_apply(repo.stash_infos.last)
40
+ #
41
+ # @return [Array<Git::StashInfo>] the stash entries, newest first; empty
42
+ # when there are none
43
+ #
44
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
45
+ #
46
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
47
+ #
48
+ def stash_infos
49
+ result = Git::Commands::Stash::List.new(@execution_context).call
50
+ Git::Parsers::Stash.parse_list(result.stdout)
51
+ end
52
+
53
+ # Option keys accepted by {#stash_push}
54
+ STASH_PUSH_ALLOWED_OPTS = %i[
55
+ patch p staged S keep_index k no_keep_index quiet q include_untracked u all a
56
+ message m pathspec_from_file pathspec_file_nul
57
+ ].freeze
58
+ private_constant :STASH_PUSH_ALLOWED_OPTS
59
+
60
+ # Save the working tree and index state to a new stash entry
61
+ #
62
+ # The stash list is read before and after the push and the entry counts are
63
+ # compared, so the return value is `nil` whenever git created no entry. This
64
+ # holds with `quiet: true`, which suppresses git's "No local changes to
65
+ # save" message.
66
+ #
67
+ # @overload stash_push(*pathspec, options = {})
68
+ #
69
+ # @example Stash all changes with a message
70
+ # info = repo.stash_push(message: 'WIP: feature work')
71
+ # info.name #=> "stash@{0}"
72
+ # info.message #=> "On main: WIP: feature work"
73
+ #
74
+ # @example Stash only specific paths
75
+ # repo.stash_push('src/a.rb', 'src/b.rb', message: 'partial work')
76
+ #
77
+ # @example Nothing to stash
78
+ # repo.stash_push #=> nil
79
+ #
80
+ # @param pathspec [Array<String>] paths that limit what gets stashed; when
81
+ # empty, all changes are stashed
82
+ #
83
+ # @param options [Hash] options for the push
84
+ #
85
+ # @option options [Boolean, nil] :patch (nil) interactively select hunks to
86
+ # stash (alias: `:p`)
87
+ #
88
+ # @option options [Boolean, nil] :p (nil) alias for `:patch`
89
+ #
90
+ # @option options [Boolean, nil] :staged (nil) stash only the staged changes
91
+ # (alias: `:S`; requires git 2.35+)
92
+ #
93
+ # @option options [Boolean, nil] :S (nil) alias for `:staged` (requires git
94
+ # 2.35+)
95
+ #
96
+ # @option options [Boolean, nil] :keep_index (nil) keep the staged changes
97
+ # in the index (alias: `:k`)
98
+ #
99
+ # @option options [Boolean, nil] :k (nil) alias for `:keep_index`
100
+ #
101
+ # @option options [Boolean, nil] :no_keep_index (nil) do not keep the staged
102
+ # changes in the index
103
+ #
104
+ # @option options [Boolean, nil] :quiet (nil) suppress informational
105
+ # messages (alias: `:q`)
106
+ #
107
+ # @option options [Boolean, nil] :q (nil) alias for `:quiet`
108
+ #
109
+ # @option options [Boolean, nil] :include_untracked (nil) include untracked
110
+ # files in the stash (alias: `:u`)
111
+ #
112
+ # @option options [Boolean, nil] :u (nil) alias for `:include_untracked`
113
+ #
114
+ # @option options [Boolean, nil] :all (nil) include untracked and ignored
115
+ # files in the stash (alias: `:a`)
116
+ #
117
+ # @option options [Boolean, nil] :a (nil) alias for `:all`
118
+ #
119
+ # @option options [String] :message (nil) the stash message (alias: `:m`)
120
+ #
121
+ # @option options [String] :m (nil) alias for `:message`
122
+ #
123
+ # @option options [String] :pathspec_from_file (nil) read pathspecs from the
124
+ # given file; pass `-` to read from standard input
125
+ #
126
+ # @option options [Boolean, nil] :pathspec_file_nul (nil) when used with
127
+ # `:pathspec_from_file`, pathspecs are NUL-separated
128
+ #
129
+ # @return [Git::StashInfo, nil] the new entry, or `nil` when there were no
130
+ # local changes to save
131
+ #
132
+ # @raise [ArgumentError] if unsupported options are provided
133
+ #
134
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
135
+ #
136
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
137
+ #
138
+ def stash_push(*pathspec)
139
+ pathspec, opts = Private.split_pathspec_and_opts(pathspec)
140
+ SharedPrivate.assert_valid_opts!(STASH_PUSH_ALLOWED_OPTS, **opts)
141
+ previous_count = stash_infos.size
142
+ Git::Commands::Stash::Push.new(@execution_context).call(*pathspec, **opts)
143
+ entries = stash_infos
144
+ entries.first if entries.size > previous_count
145
+ end
146
+
147
+ # Option keys accepted by {#stash_apply}
148
+ STASH_APPLY_ALLOWED_OPTS = %i[index quiet q].freeze
149
+ private_constant :STASH_APPLY_ALLOWED_OPTS
150
+
151
+ # Apply a stash entry to the working tree, keeping it in the stash list
152
+ #
153
+ # @example Apply the most recent entry
154
+ # repo.stash_apply #=> "On branch main\nChanges not staged for commit:..."
155
+ #
156
+ # @example Apply an entry from {#stash_infos}
157
+ # repo.stash_apply(repo.stash_infos.last)
158
+ #
159
+ # @example Apply an entry by name and restore the index too
160
+ # repo.stash_apply('stash@{1}', index: true)
161
+ #
162
+ # @param stash [Git::StashInfo, String, Integer, nil] the entry to apply: a
163
+ # {Git::StashInfo}, a `stash@{N}` name, an Integer `N` (`0` is the most
164
+ # recent entry), or `nil` for the most recent entry
165
+ #
166
+ # @param opts [Hash] options for the apply
167
+ #
168
+ # @option opts [Boolean, nil] :index (nil) restore the index state as
169
+ # well as the working tree
170
+ #
171
+ # @option opts [Boolean, nil] :quiet (nil) suppress informational
172
+ # messages (alias: `:q`)
173
+ #
174
+ # @option opts [Boolean, nil] :q (nil) alias for `:quiet`
175
+ #
176
+ # @return [String] git's stdout from the apply
177
+ #
178
+ # @raise [ArgumentError] if unsupported options are provided
179
+ #
180
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
181
+ #
182
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
183
+ #
184
+ def stash_apply(stash = nil, opts = {})
185
+ stash, opts = Private.split_stash_and_opts(stash, opts)
186
+ SharedPrivate.assert_valid_opts!(STASH_APPLY_ALLOWED_OPTS, **opts)
187
+ Git::Commands::Stash::Apply.new(@execution_context).call(stash, **opts).stdout
188
+ end
189
+
190
+ # Option keys accepted by {#stash_pop}
191
+ STASH_POP_ALLOWED_OPTS = %i[index quiet q].freeze
192
+ private_constant :STASH_POP_ALLOWED_OPTS
193
+
194
+ # Apply a stash entry to the working tree and remove it from the stash list
195
+ #
196
+ # @example Pop the most recent entry
197
+ # repo.stash_pop #=> "On branch main\n...Dropped refs/stash@{0} (abc1234...)"
198
+ #
199
+ # @example Pop an entry from {#stash_infos}
200
+ # repo.stash_pop(repo.stash_infos.last)
201
+ #
202
+ # @param stash [Git::StashInfo, String, Integer, nil] the entry to pop: a
203
+ # {Git::StashInfo}, a `stash@{N}` name, an Integer `N` (`0` is the most
204
+ # recent entry), or `nil` for the most recent entry
205
+ #
206
+ # @param opts [Hash] options for the pop
207
+ #
208
+ # @option opts [Boolean, nil] :index (nil) restore the index state as
209
+ # well as the working tree
210
+ #
211
+ # @option opts [Boolean, nil] :quiet (nil) suppress informational
212
+ # messages (alias: `:q`)
213
+ #
214
+ # @option opts [Boolean, nil] :q (nil) alias for `:quiet`
215
+ #
216
+ # @return [String] git's stdout from the pop
217
+ #
218
+ # @raise [ArgumentError] if unsupported options are provided
219
+ #
220
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
221
+ #
222
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
223
+ #
224
+ def stash_pop(stash = nil, opts = {})
225
+ stash, opts = Private.split_stash_and_opts(stash, opts)
226
+ SharedPrivate.assert_valid_opts!(STASH_POP_ALLOWED_OPTS, **opts)
227
+ Git::Commands::Stash::Pop.new(@execution_context).call(stash, **opts).stdout
228
+ end
229
+
230
+ # Option keys accepted by {#stash_drop}
231
+ STASH_DROP_ALLOWED_OPTS = %i[quiet q].freeze
232
+ private_constant :STASH_DROP_ALLOWED_OPTS
233
+
234
+ # Remove a single stash entry from the stash list
235
+ #
236
+ # @example Drop the most recent entry
237
+ # repo.stash_drop #=> "Dropped refs/stash@{0} (abc1234...)"
238
+ #
239
+ # @example Drop an entry from {#stash_infos}
240
+ # repo.stash_drop(repo.stash_infos.last)
241
+ #
242
+ # @param stash [Git::StashInfo, String, Integer, nil] the entry to drop: a
243
+ # {Git::StashInfo}, a `stash@{N}` name, an Integer `N` (`0` is the most
244
+ # recent entry), or `nil` for the most recent entry
245
+ #
246
+ # @param opts [Hash] options for the drop
247
+ #
248
+ # @option opts [Boolean, nil] :quiet (nil) suppress informational
249
+ # messages (alias: `:q`)
250
+ #
251
+ # @option opts [Boolean, nil] :q (nil) alias for `:quiet`
252
+ #
253
+ # @return [String] git's stdout from the drop
254
+ #
255
+ # @raise [ArgumentError] if unsupported options are provided
256
+ #
257
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
258
+ #
259
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
260
+ #
261
+ def stash_drop(stash = nil, opts = {})
262
+ stash, opts = Private.split_stash_and_opts(stash, opts)
263
+ SharedPrivate.assert_valid_opts!(STASH_DROP_ALLOWED_OPTS, **opts)
264
+ Git::Commands::Stash::Drop.new(@execution_context).call(stash, **opts).stdout
265
+ end
266
+
267
+ # Option keys accepted by {#stash_show}
268
+ STASH_SHOW_ALLOWED_OPTS = %i[
269
+ patch numstat raw shortstat unified U include_untracked u no_include_untracked
270
+ only_untracked find_renames M find_copies C find_copies_harder inter_hunk_context dirstat
271
+ ].freeze
272
+ private_constant :STASH_SHOW_ALLOWED_OPTS
273
+
274
+ # Show the changes recorded in a stash entry as a diff
275
+ #
276
+ # Without options, git prints a diffstat. The output is returned as git
277
+ # prints it.
278
+ #
279
+ # @example Show the diffstat of the most recent entry
280
+ # repo.stash_show #=> " file.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)"
281
+ #
282
+ # @example Show the full patch of an entry from {#stash_infos}
283
+ # repo.stash_show(repo.stash_infos.last, patch: true)
284
+ #
285
+ # @param stash [Git::StashInfo, String, Integer, nil] the entry to show: a
286
+ # {Git::StashInfo}, a `stash@{N}` name, an Integer `N` (`0` is the most
287
+ # recent entry), or `nil` for the most recent entry
288
+ #
289
+ # @param opts [Hash] options for the show
290
+ #
291
+ # @option opts [Boolean, nil] :patch (nil) show the diff as a patch
292
+ #
293
+ # @option opts [Boolean, nil] :numstat (nil) show per-file insertion and
294
+ # deletion counts
295
+ #
296
+ # @option opts [Boolean, nil] :raw (nil) show per-file mode, object id,
297
+ # and status metadata
298
+ #
299
+ # @option opts [Boolean, nil] :shortstat (nil) show only the summary line
300
+ #
301
+ # @option opts [Integer, String] :unified (nil) number of context lines
302
+ # in the patch (alias: `:U`)
303
+ #
304
+ # @option opts [Integer, String] :U (nil) alias for `:unified`
305
+ #
306
+ # @option opts [Boolean, nil] :include_untracked (nil) include the
307
+ # untracked files recorded in the entry (alias: `:u`; requires git 2.30+)
308
+ #
309
+ # @option opts [Boolean, nil] :u (nil) alias for `:include_untracked`
310
+ # (requires git 2.30+)
311
+ #
312
+ # @option opts [Boolean, nil] :no_include_untracked (nil) exclude the
313
+ # untracked files recorded in the entry (requires git 2.30+)
314
+ #
315
+ # @option opts [Boolean, nil] :only_untracked (nil) show only the
316
+ # untracked files recorded in the entry (requires git 2.30+)
317
+ #
318
+ # @option opts [Boolean, Integer, nil] :find_renames (nil) detect
319
+ # renames, optionally with a similarity threshold (alias: `:M`)
320
+ #
321
+ # @option opts [Boolean, Integer, nil] :M (nil) alias for `:find_renames`
322
+ #
323
+ # @option opts [Boolean, Integer, nil] :find_copies (nil) detect copies
324
+ # as well as renames, optionally with a similarity threshold (alias: `:C`)
325
+ #
326
+ # @option opts [Boolean, Integer, nil] :C (nil) alias for `:find_copies`
327
+ #
328
+ # @option opts [Boolean, nil] :find_copies_harder (nil) inspect unmodified
329
+ # files as copy sources
330
+ #
331
+ # @option opts [Integer, String] :inter_hunk_context (nil) number of
332
+ # context lines between hunks before they are merged
333
+ #
334
+ # @option opts [Boolean, String, nil] :dirstat (nil) include directory
335
+ # statistics, optionally with parameters
336
+ #
337
+ # @return [String] git's stdout from the show
338
+ #
339
+ # @raise [ArgumentError] if unsupported options are provided
340
+ #
341
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
342
+ #
343
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
344
+ #
345
+ def stash_show(stash = nil, opts = {})
346
+ stash, opts = Private.split_stash_and_opts(stash, opts)
347
+ SharedPrivate.assert_valid_opts!(STASH_SHOW_ALLOWED_OPTS, **opts)
348
+ Git::Commands::Stash::Show.new(@execution_context).call(stash, **opts).stdout
349
+ end
350
+
351
+ # Create and check out a branch from the commit a stash entry was based on
352
+ #
353
+ # Applies the entry on the new branch and, when that succeeds, drops the
354
+ # entry from the stash list.
355
+ #
356
+ # @example Branch from the most recent entry
357
+ # repo.stash_branch('feature') #=> "Switched to a new branch 'feature'\n..."
358
+ #
359
+ # @example Branch from an entry from {#stash_infos}
360
+ # repo.stash_branch('feature', repo.stash_infos.last)
361
+ #
362
+ # @param branch_name [String] the name of the branch to create
363
+ #
364
+ # @param stash [Git::StashInfo, String, Integer, nil] the entry to branch
365
+ # from: a {Git::StashInfo}, a `stash@{N}` name, an Integer `N` (`0` is the
366
+ # most recent entry), or `nil` for the most recent entry
367
+ #
368
+ # @return [String] git's stdout from the branch command
369
+ #
370
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
371
+ #
372
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
373
+ #
374
+ def stash_branch(branch_name, stash = nil)
375
+ Git::Commands::Stash::Branch.new(@execution_context).call(branch_name, stash).stdout
376
+ end
377
+
378
+ # Create a stash commit without adding it to the stash list
379
+ #
380
+ # The working tree and index are left unchanged. Pass the returned object
381
+ # id to {#stash_store} to add it to the stash list later.
382
+ #
383
+ # @example Create a stash commit
384
+ # repo.stash_create('WIP') #=> "3f8b2d9c..." (the full object id)
385
+ #
386
+ # @example Nothing to stash
387
+ # repo.stash_create #=> nil
388
+ #
389
+ # @param message [String, nil] the message for the stash commit; `nil` for
390
+ # git's default message
391
+ #
392
+ # @return [String, nil] the object id of the stash commit, or `nil` when
393
+ # there were no local changes
394
+ #
395
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
396
+ #
397
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
398
+ #
399
+ def stash_create(message = nil)
400
+ oid = Git::Commands::Stash::Create.new(@execution_context).call(message).stdout.strip
401
+ oid.empty? ? nil : oid
402
+ end
403
+
404
+ # Option keys accepted by {#stash_store}
405
+ STASH_STORE_ALLOWED_OPTS = %i[message m quiet q].freeze
406
+ private_constant :STASH_STORE_ALLOWED_OPTS
407
+
408
+ # Add a stash commit created by {#stash_create} to the stash list
409
+ #
410
+ # The stash list is read after the store to return the new top entry.
411
+ #
412
+ # @example Store a stash commit
413
+ # oid = repo.stash_create
414
+ # info = repo.stash_store(oid, message: 'saved for later')
415
+ # info.name #=> "stash@{0}"
416
+ # info.message #=> "saved for later"
417
+ #
418
+ # @param commit [String] the object id of the stash commit to store
419
+ #
420
+ # @param opts [Hash] options for the store
421
+ #
422
+ # @option opts [String] :message (nil) the message for the stash entry
423
+ # (alias: `:m`)
424
+ #
425
+ # @option opts [String] :m (nil) alias for `:message`
426
+ #
427
+ # @option opts [Boolean, nil] :quiet (nil) suppress informational
428
+ # messages (alias: `:q`)
429
+ #
430
+ # @option opts [Boolean, nil] :q (nil) alias for `:quiet`
431
+ #
432
+ # @return [Git::StashInfo] the stored entry, now at the top of the stash list
433
+ #
434
+ # @raise [ArgumentError] if unsupported options are provided
435
+ #
436
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
437
+ #
438
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
439
+ #
440
+ def stash_store(commit, opts = {})
441
+ SharedPrivate.assert_valid_opts!(STASH_STORE_ALLOWED_OPTS, **opts)
442
+ Git::Commands::Stash::Store.new(@execution_context).call(commit, **opts)
443
+ stash_infos.first
444
+ end
445
+
446
+ # Remove all stash entries
447
+ #
448
+ # Removes all entries from the stash list. Use with caution as this
449
+ # operation cannot be undone.
450
+ #
451
+ # @example Clear all stashes
452
+ # repo.stash_clear #=> ""
453
+ #
454
+ # @return [String] the output from the git stash clear command
455
+ # (typically empty)
456
+ #
457
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
458
+ #
459
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
460
+ #
461
+ def stash_clear
462
+ Git::Commands::Stash::Clear.new(@execution_context).call.stdout
463
+ end
464
+
15
465
  # Returns all stash entries as an array of index and message pairs
16
466
  #
17
467
  # Lists all stash entries in the repository ordered from oldest to newest.
@@ -35,12 +485,20 @@ module Git
35
485
  # reference: `'stash@{%d}' % (total - 1 - index)`, or pass the string
36
486
  # reference directly to {#stash_apply}.
37
487
  #
488
+ # @deprecated Use {#stash_infos} instead. It returns {Git::StashInfo} entries
489
+ # newest first with git's own `stash@{N}` indices and the full message.
490
+ # This method will be removed in v6.0.0.
491
+ #
492
+ # @see #stash_infos
493
+ #
38
494
  # @see https://git-scm.com/docs/git-stash git-stash documentation
39
495
  #
40
496
  def stashes_all
41
- result = Git::Commands::Stash::List.new(@execution_context).call
42
- stashes = Git::Parsers::Stash.parse_list(result.stdout)
43
- stashes.reverse.each_with_index.map do |info, i|
497
+ Git::Deprecation.warn(
498
+ 'Git::Repository#stashes_all is deprecated and will be removed in v6.0.0. ' \
499
+ 'Use Git::Repository#stash_infos instead.'
500
+ )
501
+ stash_infos.reverse.each_with_index.map do |info, i|
44
502
  message = info.message.sub(/^(?:WIP on|On)\s+[^:]+:\s*/, '')
45
503
  [i, message]
46
504
  end
@@ -57,26 +515,27 @@ module Git
57
515
  #
58
516
  # @raise [Git::FailedError] if git exits with a non-zero exit status
59
517
  #
60
- # @deprecated Use {#stashes_all} instead
518
+ # @deprecated Use {#stash_infos} instead and format the entries yourself:
519
+ # `repo.stash_infos.map { |s| "#{s.name}: #{s.message}" }.join("\n")`.
520
+ # This method will be removed in v6.0.0, and a later release will reuse
521
+ # the name for a method returning `Array<Git::StashInfo>`.
61
522
  #
62
- # @see #stashes_all
523
+ # @see #stash_infos
63
524
  #
64
525
  # @see https://git-scm.com/docs/git-stash git-stash documentation
65
526
  #
66
527
  def stash_list
67
528
  Git::Deprecation.warn(
68
529
  'Git::Repository#stash_list is deprecated and will be removed in v6.0.0. ' \
69
- 'Use Git::Repository#stashes_all instead.'
530
+ 'Use Git::Repository#stash_infos instead.'
70
531
  )
71
- result = Git::Commands::Stash::List.new(@execution_context).call
72
- stashes = Git::Parsers::Stash.parse_list(result.stdout)
73
- stashes.map { |info| "#{info.name}: #{info.message}" }.join("\n")
532
+ stash_infos.map { |info| "#{info.name}: #{info.message}" }.join("\n")
74
533
  end
75
534
 
76
535
  # Save the current working directory and index state to a new stash
77
536
  #
78
537
  # @example Save current changes
79
- # repo.stash_save('WIP: feature work')
538
+ # repo.stash_save('WIP: feature work') #=> true
80
539
  #
81
540
  # @param message [String] the stash message
82
541
  #
@@ -85,59 +544,80 @@ module Git
85
544
  #
86
545
  # @raise [Git::FailedError] if git exits with a non-zero exit status
87
546
  #
547
+ # @deprecated Use {#stash_push} with the `:message` option instead. It
548
+ # returns the new {Git::StashInfo}, or `nil` when there were no local
549
+ # changes to save. This method will be removed in v6.0.0.
550
+ #
551
+ # @see #stash_push
552
+ #
88
553
  # @see https://git-scm.com/docs/git-stash git-stash documentation
89
554
  #
90
555
  def stash_save(message) # rubocop:disable Naming/PredicateMethod
556
+ Git::Deprecation.warn(
557
+ 'Git::Repository#stash_save is deprecated and will be removed in v6.0.0. ' \
558
+ 'Use Git::Repository#stash_push(message: ...) instead.'
559
+ )
91
560
  result = Git::Commands::Stash::Push.new(@execution_context).call(message: message)
92
561
  !result.stdout.include?('No local changes to save')
93
562
  end
94
563
 
95
- # Apply a stash to the working directory
96
- #
97
- # Applies the changes recorded in a stash entry to the working directory
98
- # without removing the entry from the stash list. Unlike `git stash pop`,
99
- # the stash entry is preserved after applying.
100
- #
101
- # @example Apply the most recent stash
102
- # repo.stash_apply #=> "HEAD is now at abc1234 Initial commit"
564
+ # Private helpers local to {Git::Repository::Stashing}
103
565
  #
104
- # @example Apply a specific stash entry by reference
105
- # repo.stash_apply('stash@{1}') #=> "HEAD is now at abc1234 Initial commit"
566
+ # @api private
106
567
  #
107
- # @param id [String, Integer, nil] the stash identifier (e.g., `'stash@{0}'`,
108
- # `0`) or `nil` to apply the most recent stash entry. When an Integer is
109
- # given it is passed directly to git as `stash@{N}`, where `0` is the
110
- # **most recent** stash — the opposite order from {#stashes_all}'s
111
- # sequential indices, where `0` is the **oldest** stash.
112
- #
113
- # @return [String] the output from the git stash apply command
114
- #
115
- # @raise [Git::FailedError] if git exits with a non-zero exit status
116
- #
117
- # @see https://git-scm.com/docs/git-stash git-stash documentation
118
- #
119
- def stash_apply(id = nil)
120
- Git::Commands::Stash::Apply.new(@execution_context).call(id).stdout
121
- end
568
+ module Private
569
+ module_function
122
570
 
123
- # Remove all stash entries
124
- #
125
- # Removes all entries from the stash list. Use with caution as this
126
- # operation cannot be undone.
127
- #
128
- # @example Clear all stashes
129
- # repo.stash_clear #=> ""
130
- #
131
- # @return [String] the output from the git stash clear command
132
- # (typically empty)
133
- #
134
- # @raise [Git::FailedError] if git exits with a non-zero exit status
135
- #
136
- # @see https://git-scm.com/docs/git-stash git-stash documentation
137
- #
138
- def stash_clear
139
- Git::Commands::Stash::Clear.new(@execution_context).call.stdout
571
+ # Separate the stash operand from a positional options Hash
572
+ #
573
+ # The stash-taking facade methods are declared `(stash = nil, opts = {})`.
574
+ # When the caller omits the stash and passes only options
575
+ # (`repo.stash_apply(index: true)`), Ruby binds the Hash to `stash`; this
576
+ # moves it to `opts` so both call shapes reach the command the same way.
577
+ #
578
+ # @example Options passed without a stash
579
+ # Private.split_stash_and_opts({ index: true }, {}) #=> [nil, { index: true }]
580
+ #
581
+ # @example A stash and options
582
+ # Private.split_stash_and_opts('stash@{1}', { index: true }) #=> ["stash@{1}", { index: true }]
583
+ #
584
+ # @param stash [Git::StashInfo, String, Integer, Hash, nil] the stash
585
+ # operand, or the options Hash when the stash was omitted
586
+ #
587
+ # @param trailing [Hash] the second positional argument as bound by Ruby
588
+ #
589
+ # @return [Array(Object, Hash)] the stash operand and the options Hash
590
+ #
591
+ def split_stash_and_opts(stash, trailing)
592
+ return [nil, stash] if stash.is_a?(Hash) && trailing.empty?
593
+
594
+ [stash, trailing]
595
+ end
596
+
597
+ # Separate a trailing options Hash from a pathspec list
598
+ #
599
+ # {Git::Repository::Stashing#stash_push} takes a variadic pathspec, so
600
+ # its options arrive as the last positional argument when present.
601
+ #
602
+ # @example Pathspecs followed by options
603
+ # Private.split_pathspec_and_opts(['a.rb', { message: 'WIP' }])
604
+ # #=> [["a.rb"], { message: "WIP" }]
605
+ #
606
+ # @example Pathspecs only
607
+ # Private.split_pathspec_and_opts(['a.rb']) #=> [["a.rb"], {}]
608
+ #
609
+ # @param pathspec [Array<String, Hash>] the positional arguments, possibly
610
+ # ending in an options Hash
611
+ #
612
+ # @return [Array(Array<String>, Hash)] the pathspecs and the options Hash
613
+ #
614
+ def split_pathspec_and_opts(pathspec)
615
+ return [pathspec[0...-1], pathspec.last] if pathspec.last.is_a?(Hash)
616
+
617
+ [pathspec, {}]
618
+ end
140
619
  end
620
+ private_constant :Private
141
621
  end
142
622
  end
143
623
  end