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.
@@ -2,8 +2,11 @@
2
2
 
3
3
  require 'git/commands/ls_files'
4
4
  require 'git/commands/rev_parse'
5
+ require 'git/commands/status'
5
6
  require 'git/escaped_path'
7
+ require 'git/parsers/status'
6
8
  require 'git/status'
9
+ require 'git/status_info'
7
10
 
8
11
  module Git
9
12
  class Repository
@@ -87,6 +90,47 @@ module Git
87
90
  ).stdout.split("\n").map { |f| Private.unescape_quoted_path(f) }
88
91
  end
89
92
 
93
+ # Returns a {Git::StatusInfo} describing the index and working tree state
94
+ #
95
+ # Runs `git status` in porcelain v2 format with NUL-separated entries and
96
+ # every untracked file listed individually, then reads `core.ignoreCase`
97
+ # as a boolean so that the path predicates on the result compare paths the
98
+ # way git does in this repository. Every entry type git reports is
99
+ # represented, including renames, copies, and merge conflicts. Clean
100
+ # tracked paths are not reported by `git status`, so they are absent from
101
+ # the result; the deprecated {#status} listed them, and {#ls_files} still
102
+ # does.
103
+ #
104
+ # @example Check which files are modified
105
+ # repo.status_info.changed
106
+ # #=> { "lib/foo.rb" => #<data Git::StatusFileInfo path="lib/foo.rb", ...> }
107
+ #
108
+ # @example Check for untracked files
109
+ # repo.status_info.untracked.keys #=> ["new_file.rb"]
110
+ #
111
+ # @example Check one path
112
+ # repo.status_info.changed?('lib/foo.rb') #=> true
113
+ #
114
+ # @example Iterate over every entry
115
+ # repo.status_info.files.each do |file|
116
+ # puts "#{file.index_status}#{file.worktree_status} #{file.path}"
117
+ # end
118
+ #
119
+ # @return [Git::StatusInfo] the status of the repository
120
+ #
121
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
122
+ #
123
+ # @see https://git-scm.com/docs/git-status git-status
124
+ #
125
+ def status_info
126
+ result = Git::Commands::Status.new(@execution_context).call(
127
+ porcelain: 'v2', z: true, untracked_files: 'all'
128
+ )
129
+ files = Git::Parsers::Status.parse(result.stdout)
130
+ ignore_case = config_get('core.ignoreCase', type: 'bool')&.value == 'true'
131
+ Git::StatusInfo.new(files: files, ignore_case: ignore_case)
132
+ end
133
+
90
134
  # Returns a {Git::Status} object describing the working tree and index state
91
135
  #
92
136
  # Constructs a {Git::Status} for this repository by collecting information from
@@ -95,22 +139,26 @@ module Git
95
139
  # result identifies which files have been modified, added, deleted, or are
96
140
  # untracked.
97
141
  #
98
- # @example Check which files are modified
99
- # repo.status.changed #=> { "lib/foo.rb" => <Git::Status::StatusFile ...> }
100
- #
101
- # @example Check for untracked files
102
- # repo.status.untracked #=> { "new_file.rb" => <Git::Status::StatusFile ...> }
142
+ # Emits one deprecation warning per call. The {Git::Status} it constructs is
143
+ # built with warnings silenced so the caller does not see a second one.
103
144
  #
104
- # @example Iterate over all status files
105
- # repo.status.each { |file| puts "#{file.path}: #{file.type}" }
145
+ # @example Check which files are modified (deprecated; use status_info)
146
+ # repo.status.changed.keys #=> ["lib/foo.rb"]
147
+ # repo.status_info.changed.keys #=> ["lib/foo.rb"]
106
148
  #
107
149
  # @return [Git::Status] the status of the repository
108
150
  #
109
151
  # @raise [Git::FailedError] if any underlying git command exits with a
110
152
  # non-zero exit status
111
153
  #
154
+ # @deprecated Use {#status_info} instead
155
+ #
112
156
  def status
113
- Git::Status.new(self)
157
+ Git::Deprecation.warn(
158
+ 'Git::Repository#status is deprecated and will be removed in v6.0.0. ' \
159
+ 'Use Git::Repository#status_info instead.'
160
+ )
161
+ Git::Deprecation.silence { Git::Status.new(self) }
114
162
  end
115
163
 
116
164
  # List all files tracked in the index
@@ -1,22 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'git/commands/worktree'
4
+ require 'git/parsers/worktree'
4
5
  require 'git/worktree'
6
+ require 'git/worktree_info'
5
7
  require 'git/worktrees'
6
8
 
7
9
  module Git
8
10
  class Repository
9
- # Facade methods for worktree operations
11
+ # Facade methods for worktree operations: listing, adding, removing, moving,
12
+ # locking, repairing, and pruning worktrees
10
13
  #
11
14
  # Included by {Git::Repository}.
12
15
  #
13
16
  # @api private
14
17
  #
15
18
  module WorktreeOperations
19
+ # Returns every worktree attached to the repository
20
+ #
21
+ # Lists the main worktree first, then each linked worktree, in the order
22
+ # git reports them. The main worktree of a bare repository is included
23
+ # with {Git::WorktreeInfo#bare?} true and no head or branch.
24
+ #
25
+ # @example List all worktrees
26
+ # repo.worktree_list.map(&:path)
27
+ # #=> ["/path/to/main", "/tmp/feature"]
28
+ #
29
+ # @example Find the worktree that has a branch checked out
30
+ # info = repo.worktree_list.find { |w| w.branch == 'refs/heads/feature' }
31
+ # info.path #=> "/tmp/feature"
32
+ # info.head #=> "b8c63202c3c0ebd37b7e45fd0c22e6c20d5bead1"
33
+ # info.locked? #=> false
34
+ #
35
+ # @return [Array<Git::WorktreeInfo>] one entry per worktree
36
+ #
37
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
38
+ #
39
+ # @raise [Git::UnexpectedResultError] if the worktree listing cannot be
40
+ # parsed
41
+ #
42
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
43
+ #
44
+ def worktree_list
45
+ result = Git::Commands::Worktree::List.new(@execution_context).call(porcelain: true)
46
+ Git::Parsers::Worktree.parse_list(result.stdout)
47
+ end
48
+
16
49
  # Returns all worktrees as an array of directory and SHA pairs
17
50
  #
18
- # Lists all worktrees attached to the repository, including the main
19
- # worktree and all linked worktrees.
51
+ # Lists the main worktree and all linked worktrees. The main worktree of
52
+ # a bare repository has no checked-out commit and is omitted.
20
53
  #
21
54
  # @example List all worktrees
22
55
  # repo.worktrees_all
@@ -30,20 +63,22 @@ module Git
30
63
  #
31
64
  # @raise [Git::FailedError] if git exits with a non-zero exit status
32
65
  #
66
+ # @deprecated Use {#worktree_list} instead
67
+ #
68
+ # {#worktree_list} returns one {Git::WorktreeInfo} per worktree, with
69
+ # `path` and `head` in place of the pair, and includes the main worktree
70
+ # of a bare repository.
71
+ #
72
+ # @see #worktree_list
73
+ #
33
74
  # @see https://git-scm.com/docs/git-worktree git-worktree documentation
34
75
  #
35
76
  def worktrees_all
36
- worktree_entries = []
37
- current_directory = ''
38
- command_output = Git::Commands::Worktree::List.new(@execution_context).call(porcelain: true).stdout
39
-
40
- command_output.each_line(chomp: true) do |line|
41
- key, value = line.split(' ', 2)
42
- current_directory = value if key == 'worktree'
43
- worktree_entries << [current_directory, value] if key == 'HEAD'
44
- end
45
-
46
- worktree_entries
77
+ Git::Deprecation.warn(
78
+ 'Git::Repository#worktrees_all is deprecated and will be removed in v6.0.0. ' \
79
+ 'Use Git::Repository#worktree_list instead.'
80
+ )
81
+ worktree_list.reject { |worktree| worktree.head.nil? }.map { |worktree| [worktree.path, worktree.head] }
47
82
  end
48
83
 
49
84
  # Create a new linked worktree at the given directory
@@ -75,10 +110,15 @@ module Git
75
110
 
76
111
  # Remove a linked worktree
77
112
  #
78
- # @example Remove a worktree
113
+ # @example Remove a worktree by path
79
114
  # repo.worktree_remove('/tmp/feature')
80
115
  #
81
- # @param dir [String] filesystem path of the worktree to remove
116
+ # @example Remove a worktree from the list
117
+ # info = repo.worktree_list.find { |w| w.branch == 'refs/heads/feature' }
118
+ # repo.worktree_remove(info)
119
+ #
120
+ # @param worktree [String, Git::WorktreeInfo] the path of the worktree to
121
+ # remove, or its entry from {#worktree_list}
82
122
  #
83
123
  # @return [String] the output from the git worktree remove command
84
124
  # (typically empty)
@@ -87,8 +127,120 @@ module Git
87
127
  #
88
128
  # @see https://git-scm.com/docs/git-worktree git-worktree documentation
89
129
  #
90
- def worktree_remove(dir)
91
- Git::Commands::Worktree::Remove.new(@execution_context).call(dir).stdout
130
+ def worktree_remove(worktree)
131
+ Git::Commands::Worktree::Remove.new(@execution_context).call(worktree.to_s).stdout
132
+ end
133
+
134
+ # Move a linked worktree to a new location
135
+ #
136
+ # @example Move a worktree
137
+ # repo.worktree_move('/tmp/feature', '/tmp/feature-moved')
138
+ #
139
+ # @example Move a locked worktree
140
+ # repo.worktree_move('/tmp/feature', '/tmp/feature-moved', force: 2)
141
+ #
142
+ # @param worktree [String, Git::WorktreeInfo] the path of the worktree to
143
+ # move, or its entry from {#worktree_list}
144
+ #
145
+ # @param new_path [String] the destination path
146
+ #
147
+ # @param opts [Hash] options for the move
148
+ #
149
+ # @option opts [Boolean, Integer, nil] :force (nil) override git's
150
+ # safeguards; git refuses to move a locked worktree unless the flag is
151
+ # given twice, so pass `2` for that
152
+ #
153
+ # @return [String] the output from the git worktree move command
154
+ # (typically empty)
155
+ #
156
+ # @raise [ArgumentError] if unsupported options are provided
157
+ #
158
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
159
+ #
160
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
161
+ #
162
+ def worktree_move(worktree, new_path, opts = {})
163
+ Git::Commands::Worktree::Move.new(@execution_context).call(worktree.to_s, new_path, **opts).stdout
164
+ end
165
+
166
+ # Lock a linked worktree so that `git worktree prune` leaves it alone
167
+ #
168
+ # Lock a worktree whose directory is on removable media or a network
169
+ # share that is not always mounted.
170
+ #
171
+ # @example Lock a worktree
172
+ # repo.worktree_lock('/tmp/feature')
173
+ #
174
+ # @example Lock a worktree with a reason
175
+ # repo.worktree_lock('/tmp/feature', reason: 'on an external drive')
176
+ #
177
+ # @param worktree [String, Git::WorktreeInfo] the path of the worktree to
178
+ # lock, or its entry from {#worktree_list}
179
+ #
180
+ # @param opts [Hash] options for the lock
181
+ #
182
+ # @option opts [String, nil] :reason (nil) an explanation stored with the
183
+ # lock and reported as {Git::WorktreeInfo#lock_reason}
184
+ #
185
+ # @return [String] the output from the git worktree lock command
186
+ # (typically empty)
187
+ #
188
+ # @raise [ArgumentError] if unsupported options are provided
189
+ #
190
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
191
+ #
192
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
193
+ #
194
+ def worktree_lock(worktree, opts = {})
195
+ Git::Commands::Worktree::Lock.new(@execution_context).call(worktree.to_s, **opts).stdout
196
+ end
197
+
198
+ # Unlock a linked worktree
199
+ #
200
+ # @example Unlock a worktree
201
+ # repo.worktree_unlock('/tmp/feature')
202
+ #
203
+ # @param worktree [String, Git::WorktreeInfo] the path of the worktree to
204
+ # unlock, or its entry from {#worktree_list}
205
+ #
206
+ # @return [String] the output from the git worktree unlock command
207
+ # (typically empty)
208
+ #
209
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
210
+ #
211
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
212
+ #
213
+ def worktree_unlock(worktree)
214
+ Git::Commands::Worktree::Unlock.new(@execution_context).call(worktree.to_s).stdout
215
+ end
216
+
217
+ # Repair the links between the repository and its linked worktrees
218
+ #
219
+ # With no paths, repairs the link from each linked worktree back to the
220
+ # repository, which is needed after the repository directory was moved.
221
+ # Given the current paths of linked worktrees that were moved without
222
+ # {#worktree_move}, also repairs the repository's links to them.
223
+ #
224
+ # @example Repair after the repository directory was moved
225
+ # repo.worktree_repair
226
+ #
227
+ # @example Repair after a linked worktree was moved by hand
228
+ # repo.worktree_repair('/new/path/to/feature')
229
+ #
230
+ # @param paths [Array<String, Git::WorktreeInfo>] the current paths of the
231
+ # worktrees to repair, or their entries from {#worktree_list}
232
+ #
233
+ # @return [String] the output from the git worktree repair command, which
234
+ # reports each repair made
235
+ #
236
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
237
+ #
238
+ # @raise [Git::VersionError] if the installed git is older than 2.29.0
239
+ #
240
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
241
+ #
242
+ def worktree_repair(*paths)
243
+ Git::Commands::Worktree::Repair.new(@execution_context).call(*paths.map(&:to_s)).stdout
92
244
  end
93
245
 
94
246
  # Prune stale worktree administrative files
@@ -128,7 +280,22 @@ module Git
128
280
  #
129
281
  # @return [Git::Worktree] a worktree domain object for the given path
130
282
  #
283
+ # @deprecated Use {#worktree_add} and {#worktree_remove} instead
284
+ #
285
+ # `repo.worktree(dir, commitish).add` becomes
286
+ # `repo.worktree_add(dir, commitish)` and `repo.worktree(dir).remove`
287
+ # becomes `repo.worktree_remove(dir)`. Read a worktree's checked-out
288
+ # commit from {Git::WorktreeInfo#head} via {#worktree_list}.
289
+ #
290
+ # @see #worktree_add
291
+ #
292
+ # @see #worktree_remove
293
+ #
131
294
  def worktree(dir, commitish = nil)
295
+ Git::Deprecation.warn(
296
+ 'Git::Repository#worktree is deprecated and will be removed in v6.0.0. ' \
297
+ 'Use Git::Repository#worktree_add and Git::Repository#worktree_remove instead.'
298
+ )
132
299
  Git::Worktree.new(self, dir, commitish)
133
300
  end
134
301
 
@@ -151,7 +318,20 @@ module Git
151
318
  #
152
319
  # @raise [Git::FailedError] if git exits with a non-zero exit status
153
320
  #
321
+ # @deprecated Use {#worktree_list} instead
322
+ #
323
+ # {#worktree_list} returns `Array<Git::WorktreeInfo>`. Look a worktree up
324
+ # by path with `worktree_list.find { |w| w.path == path }` in place of
325
+ # `worktrees[path]`, and call {#worktree_prune} in place of
326
+ # `worktrees.prune`. Calling this method emits one deprecation warning.
327
+ #
328
+ # @see #worktree_list
329
+ #
154
330
  def worktrees
331
+ Git::Deprecation.warn(
332
+ 'Git::Repository#worktrees is deprecated and will be removed in v6.0.0. ' \
333
+ 'Use Git::Repository#worktree_list instead.'
334
+ )
155
335
  Git::Worktrees.new(self)
156
336
  end
157
337
  end
data/lib/git/stash.rb CHANGED
@@ -3,11 +3,25 @@
3
3
  module Git
4
4
  # Represents a single stash entry in a Git repository
5
5
  #
6
- # @example Create a stash and inspect the result
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_push} replaces `Git::Stash.new(repo, message)` and
9
+ # returns a {Git::StashInfo}, or `nil` when there was nothing to stash.
10
+ #
11
+ # @example Create a stash and inspect the result (deprecated)
7
12
  # stash = Git::Stash.new(repo, 'WIP: feature work')
8
13
  # stash.message #=> "WIP: feature work"
9
14
  # stash.saved? #=> true
10
15
  #
16
+ # @example The replacement
17
+ # info = repo.stash_push(message: 'WIP: feature work')
18
+ # info.message #=> "On main: WIP: feature work"
19
+ # info.nil? #=> false
20
+ #
21
+ # @deprecated Use {Git::Repository#stash_push} and {Git::StashInfo} instead
22
+ #
23
+ # @see Git::Repository#stash_push
24
+ #
11
25
  # @api public
12
26
  #
13
27
  class Stash
@@ -16,6 +30,8 @@ module Git
16
30
  # When `existing` is `false` (the default), immediately calls {#save} to push
17
31
  # the current working-directory state onto the stash stack.
18
32
  #
33
+ # Emits one deprecation warning per object.
34
+ #
19
35
  # @example Create a new stash entry
20
36
  # stash = Git::Stash.new(repo, 'WIP: feature work')
21
37
  # stash.saved? #=> true
@@ -32,7 +48,15 @@ module Git
32
48
  # without pushing any changes
33
49
  #
34
50
  # @return [void]
51
+ #
52
+ # @deprecated Use {Git::Repository#stash_push} and {Git::StashInfo} instead
53
+ #
35
54
  def initialize(base, message, existing: false)
55
+ Git::Deprecation.warn(
56
+ 'Git::Stash is deprecated and will be removed in v6.0.0. ' \
57
+ 'Use the Git::Repository stash methods (stash_push, stash_infos, stash_apply) ' \
58
+ 'and Git::StashInfo instead.'
59
+ )
36
60
  @base = base
37
61
  @message = message
38
62
  save unless existing
@@ -48,8 +72,10 @@ module Git
48
72
  # local changes to save
49
73
  #
50
74
  # @raise [Git::FailedError] if git exits with a non-zero exit status
75
+ #
51
76
  def save
52
- @saved = stash_repository.stash_save(@message)
77
+ # stash_save is deprecated too; silence it so one Git::Stash call emits one warning
78
+ @saved = Git::Deprecation.silence { stash_repository.stash_save(@message) }
53
79
  end
54
80
 
55
81
  # Returns whether the stash was saved successfully
@@ -60,6 +86,7 @@ module Git
60
86
  #
61
87
  # @return [Boolean, nil] `true` if changes were stashed, `false` if there were no
62
88
  # local changes, `nil` if {#save} has not been called (e.g. `existing: true`)
89
+ #
63
90
  def saved?
64
91
  @saved
65
92
  end
@@ -71,6 +98,7 @@ module Git
71
98
  # stash.message #=> "WIP: feature work"
72
99
  #
73
100
  # @return [String] the stash message
101
+ #
74
102
  attr_reader :message
75
103
 
76
104
  # Returns the stash description as a string
@@ -80,6 +108,7 @@ module Git
80
108
  # stash.to_s #=> "WIP: feature work"
81
109
  #
82
110
  # @return [String] the stash message
111
+ #
83
112
  def to_s
84
113
  message
85
114
  end
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
5
+ require 'git/author_info'
6
+
3
7
  module Git
4
8
  # Immutable value object representing stash entry information
5
9
  #
6
10
  # StashInfo encapsulates the parsed data from `git stash list` output.
7
11
  # Each entry contains comprehensive information about the stash including
8
- # its index, reference name, commit SHA, branch, message, author/committer
9
- # details, and timestamps.
12
+ # its index, reference name, commit SHA, branch, message, and the author and
13
+ # committer identities.
10
14
  #
15
+ # The author and committer are nested {Git::AuthorInfo} values. Their `date`
16
+ # is a `Time` parsed from git's ISO 8601 output, not an ISO 8601 string.
11
17
  #
12
18
  # @example Create a StashInfo from parsed stash list output
13
19
  # info = Git::StashInfo.new(
@@ -17,12 +23,16 @@ module Git
17
23
  # short_oid: 'abc123d',
18
24
  # branch: 'main',
19
25
  # message: 'WIP on main: abc123 Initial commit',
20
- # author_name: 'Jane Doe',
21
- # author_email: 'jane@example.com',
22
- # author_date: '2026-01-24T10:30:00-08:00',
23
- # committer_name: 'Jane Doe',
24
- # committer_email: 'jane@example.com',
25
- # committer_date: '2026-01-24T10:30:00-08:00'
26
+ # author: Git::AuthorInfo.new(
27
+ # name: 'Jane Doe',
28
+ # email: 'jane@example.com',
29
+ # date: Time.iso8601('2026-01-24T10:30:00-08:00')
30
+ # ),
31
+ # committer: Git::AuthorInfo.new(
32
+ # name: 'Jane Doe',
33
+ # email: 'jane@example.com',
34
+ # date: Time.iso8601('2026-01-24T10:30:00-08:00')
35
+ # )
26
36
  # )
27
37
  #
28
38
  # info.index # => 0
@@ -31,12 +41,12 @@ module Git
31
41
  # info.short_oid # => 'abc123d'
32
42
  # info.branch # => 'main'
33
43
  # info.message # => 'WIP on main: abc123 Initial commit'
34
- # info.author_name # => 'Jane Doe'
35
- # info.author_email # => 'jane@example.com'
36
- # info.author_date # => '2026-01-24T10:30:00-08:00'
37
- # info.committer_name # => 'Jane Doe'
38
- # info.committer_email # => 'jane@example.com'
39
- # info.committer_date # => '2026-01-24T10:30:00-08:00'
44
+ # info.author.name # => 'Jane Doe'
45
+ # info.author.email # => 'jane@example.com'
46
+ # info.author.date # => 2026-01-24 10:30:00 -0800
47
+ # info.committer.name # => 'Jane Doe'
48
+ #
49
+ # @see Git::AuthorInfo for the nested author and committer identities
40
50
  #
41
51
  # @api public
42
52
  #
@@ -59,23 +69,15 @@ module Git
59
69
  # @!attribute [r] message
60
70
  # @return [String] the stash message (e.g., 'WIP on main: abc123 commit msg')
61
71
  #
62
- # @!attribute [r] author_name
63
- # @return [String] the name of the stash author
64
- #
65
- # @!attribute [r] author_email
66
- # @return [String] the email of the stash author
67
- #
68
- # @!attribute [r] author_date
69
- # @return [String] the author date in ISO 8601 format
72
+ # @!attribute [r] author
73
+ # The identity of the stash author; the nested `date` is a `Time`.
70
74
  #
71
- # @!attribute [r] committer_name
72
- # @return [String] the name of the stash committer
75
+ # @return [Git::AuthorInfo] the author of the stash commit
73
76
  #
74
- # @!attribute [r] committer_email
75
- # @return [String] the email of the stash committer
77
+ # @!attribute [r] committer
78
+ # The identity of the stash committer; the nested `date` is a `Time`.
76
79
  #
77
- # @!attribute [r] committer_date
78
- # @return [String] the committer date in ISO 8601 format
80
+ # @return [Git::AuthorInfo] the committer of the stash commit
79
81
  #
80
82
  StashInfo = Data.define(
81
83
  :index,
@@ -84,12 +86,8 @@ module Git
84
86
  :short_oid,
85
87
  :branch,
86
88
  :message,
87
- :author_name,
88
- :author_email,
89
- :author_date,
90
- :committer_name,
91
- :committer_email,
92
- :committer_date
89
+ :author,
90
+ :committer
93
91
  ) do
94
92
  # Returns the stash reference name
95
93
  #