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.
@@ -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
data/lib/git/tag_info.rb CHANGED
@@ -43,7 +43,8 @@ module Git
43
43
  # info.lightweight? #=> true
44
44
  # info.tagger #=> nil
45
45
  #
46
- # @see Git::Tag for the full-featured tag object with operations
46
+ # @see Git::Repository::ObjectOperations#tag_list for the repository method
47
+ # that returns these
47
48
  #
48
49
  # @see Git::Commands::Tag::List for the command that produces these
49
50
  #
data/lib/git/version.rb CHANGED
@@ -4,7 +4,7 @@ module Git
4
4
  # The current gem version
5
5
  #
6
6
  # @return [String] the current gem version
7
- VERSION = '5.3.0'
7
+ VERSION = '5.4.0'
8
8
 
9
9
  # Represents a git version with major, minor, and patch components
10
10
  #
data/lib/git/worktree.rb CHANGED
@@ -12,6 +12,17 @@ module Git
12
12
  # worktree.add
13
13
  # worktree.remove
14
14
  #
15
+ # @deprecated Use {Git::Repository::WorktreeOperations#worktree_list} and the
16
+ # path-based worktree operations on {Git::Repository} instead
17
+ #
18
+ # {Git::Repository::WorktreeOperations#worktree_list} returns immutable
19
+ # {Git::WorktreeInfo} value objects. Operations that lived on this class are
20
+ # called on the repository with the worktree path instead (for example
21
+ # {Git::Repository::WorktreeOperations#worktree_add} and
22
+ # {Git::Repository::WorktreeOperations#worktree_remove}). {#gcommit},
23
+ # {#add}, and {#remove} each emit a deprecation warning; the `dir`, `full`,
24
+ # `to_s`, and `to_a` readers do not.
25
+ #
15
26
  # @api public
16
27
  #
17
28
  class Worktree
@@ -69,7 +80,19 @@ module Git
69
80
  # @raise [Git::FailedError] if git must resolve the commit and exits with a
70
81
  # non-zero exit status
71
82
  #
83
+ # @deprecated Use {Git::WorktreeInfo#head} from
84
+ # {Git::Repository::WorktreeOperations#worktree_list} instead
85
+ #
86
+ # `head` is always the commit SHA as a `String` (or `nil` for a bare main
87
+ # worktree). Call `repo.gcommit(info.head)` for the commit object.
88
+ #
89
+ # @see Git::WorktreeInfo#head
90
+ #
72
91
  def gcommit
92
+ Git::Deprecation.warn(
93
+ 'Git::Worktree#gcommit is deprecated and will be removed in v6.0.0. ' \
94
+ 'Use Git::WorktreeInfo#head from Git::Repository#worktree_list instead.'
95
+ )
73
96
  @gcommit ||= worktree_repository.gcommit(@full)
74
97
  @gcommit
75
98
  end
@@ -87,7 +110,15 @@ module Git
87
110
  #
88
111
  # @raise [Git::FailedError] if git exits with a non-zero exit status
89
112
  #
113
+ # @deprecated Use {Git::Repository::WorktreeOperations#worktree_add} instead
114
+ #
115
+ # @see Git::Repository::WorktreeOperations#worktree_add
116
+ #
90
117
  def add
118
+ Git::Deprecation.warn(
119
+ 'Git::Worktree#add is deprecated and will be removed in v6.0.0. ' \
120
+ 'Use Git::Repository#worktree_add instead.'
121
+ )
91
122
  worktree_repository.worktree_add(@dir, @gcommit)
92
123
  end
93
124
 
@@ -102,7 +133,15 @@ module Git
102
133
  #
103
134
  # @raise [Git::FailedError] if git exits with a non-zero exit status
104
135
  #
136
+ # @deprecated Use {Git::Repository::WorktreeOperations#worktree_remove} instead
137
+ #
138
+ # @see Git::Repository::WorktreeOperations#worktree_remove
139
+ #
105
140
  def remove
141
+ Git::Deprecation.warn(
142
+ 'Git::Worktree#remove is deprecated and will be removed in v6.0.0. ' \
143
+ 'Use Git::Repository#worktree_remove instead.'
144
+ )
106
145
  worktree_repository.worktree_remove(@dir)
107
146
  end
108
147
 
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Immutable value object for one entry of `git worktree list`
5
+ #
6
+ # Each entry carries what `git worktree list --porcelain` reports for a
7
+ # worktree: its path, the checked-out HEAD and branch, and whether it is bare,
8
+ # detached, locked, or prunable, with the reason git gives for the last two.
9
+ #
10
+ # @example A locked linked worktree with a branch checked out
11
+ # info = Git::WorktreeInfo.new(
12
+ # path: '/tmp/wt/linked',
13
+ # head: 'f3e2c1ffb860086504eeb27b77a1d0028b68fd8f',
14
+ # branch: 'refs/heads/linked',
15
+ # bare: false,
16
+ # detached: false,
17
+ # locked: true,
18
+ # lock_reason: 'on purpose',
19
+ # prunable: false,
20
+ # prune_reason: nil
21
+ # )
22
+ #
23
+ # info.path # => '/tmp/wt/linked'
24
+ # info.head # => 'f3e2c1ffb860086504eeb27b77a1d0028b68fd8f'
25
+ # info.branch # => 'refs/heads/linked'
26
+ # info.locked? # => true
27
+ # info.lock_reason # => 'on purpose'
28
+ # info.detached? # => false
29
+ # info.to_s # => '/tmp/wt/linked'
30
+ #
31
+ # @example Pass an entry back to a worktree operation
32
+ # info = repo.worktree_list.find { |w| w.branch == 'refs/heads/linked' }
33
+ # repo.worktree_remove(info)
34
+ #
35
+ # @see Git::Repository::WorktreeOperations#worktree_list for the repository
36
+ # method that returns these
37
+ #
38
+ # @api public
39
+ #
40
+ # @!attribute [r] path
41
+ # @return [String] the worktree directory as git reports it
42
+ #
43
+ # @!attribute [r] head
44
+ # @return [String, nil] the full object ID of the checked-out HEAD commit
45
+ # (the all-zero object ID when the branch has no commits yet), or nil for a
46
+ # bare main worktree
47
+ #
48
+ # @!attribute [r] branch
49
+ # @return [String, nil] the full refname of the checked-out branch (e.g.,
50
+ # 'refs/heads/main'), or nil when the worktree is bare or detached
51
+ #
52
+ # @!attribute [r] bare
53
+ # @return [Boolean] true if this is the main worktree of a bare repository
54
+ #
55
+ # @!attribute [r] detached
56
+ # @return [Boolean] true if HEAD is detached in this worktree
57
+ #
58
+ # @!attribute [r] locked
59
+ # @return [Boolean] true if the worktree is locked
60
+ #
61
+ # @!attribute [r] lock_reason
62
+ # @return [String, nil] the reason given when the worktree was locked, or nil
63
+ # when it is not locked or was locked without a reason
64
+ #
65
+ # @!attribute [r] prunable
66
+ # @return [Boolean] true if `git worktree prune` would remove this entry
67
+ #
68
+ # @!attribute [r] prune_reason
69
+ # @return [String, nil] git's explanation of why the entry is prunable, or
70
+ # nil when it is not prunable
71
+ #
72
+ WorktreeInfo = Data.define(
73
+ :path,
74
+ :head,
75
+ :branch,
76
+ :bare,
77
+ :detached,
78
+ :locked,
79
+ :lock_reason,
80
+ :prunable,
81
+ :prune_reason
82
+ ) do
83
+ # Whether this is the main worktree of a bare repository
84
+ #
85
+ # @example
86
+ # info.bare? # => false
87
+ #
88
+ # @return [Boolean] true if the worktree is bare
89
+ def bare? = bare
90
+
91
+ # Whether HEAD is detached in this worktree
92
+ #
93
+ # @example
94
+ # info.detached? # => false
95
+ #
96
+ # @return [Boolean] true if HEAD is detached
97
+ def detached? = detached
98
+
99
+ # Whether the worktree is locked
100
+ #
101
+ # @example
102
+ # info.locked? # => true
103
+ #
104
+ # @return [Boolean] true if the worktree is locked
105
+ def locked? = locked
106
+
107
+ # Whether `git worktree prune` would remove this entry
108
+ #
109
+ # @example
110
+ # info.prunable? # => false
111
+ #
112
+ # @return [Boolean] true if the entry is prunable
113
+ def prunable? = prunable
114
+
115
+ # Returns the worktree path
116
+ #
117
+ # Lets an entry be passed directly to the worktree operations that take a
118
+ # path, such as {Git::Repository::WorktreeOperations#worktree_remove}.
119
+ #
120
+ # @example Convert to string
121
+ # info.to_s # => '/tmp/wt/linked'
122
+ #
123
+ # @return [String] the worktree path
124
+ def to_s
125
+ path
126
+ end
127
+ end
128
+ end
data/lib/git/worktrees.rb CHANGED
@@ -10,6 +10,15 @@ module Git
10
10
  # worktrees = repo.worktrees
11
11
  # worktrees.each { |wt| puts wt.dir }
12
12
  #
13
+ # @deprecated Use {Git::Repository::WorktreeOperations#worktree_list} instead
14
+ #
15
+ # {Git::Repository::WorktreeOperations#worktree_list} returns
16
+ # `Array<Git::WorktreeInfo>` (immutable value objects). Look a worktree up
17
+ # by path with `worktree_list.find { |w| w.path == path }` in place of
18
+ # {#[]}, and call {Git::Repository::WorktreeOperations#worktree_prune} in
19
+ # place of {#prune}. Constructing a `Git::Worktrees` emits a deprecation
20
+ # warning.
21
+ #
13
22
  # @api public
14
23
  #
15
24
  class Worktrees
@@ -24,12 +33,21 @@ module Git
24
33
  #
25
34
  # @raise [Git::FailedError] if git exits with a non-zero exit status
26
35
  #
36
+ # @deprecated Use {Git::Repository::WorktreeOperations#worktree_list} instead
37
+ #
38
+ # @see Git::Repository::WorktreeOperations#worktree_list
39
+ #
27
40
  def initialize(base)
41
+ Git::Deprecation.warn(
42
+ 'Git::Worktrees is deprecated and will be removed in v6.0.0. ' \
43
+ 'Use Git::Repository#worktree_list instead.'
44
+ )
28
45
  @worktrees = {}
29
46
 
30
47
  @base = base
31
48
 
32
- worktree_repository.worktrees_all.each do |w|
49
+ # worktrees_all is deprecated too; silence it so one Git::Worktrees.new emits one warning
50
+ Git::Deprecation.silence { worktree_repository.worktrees_all }.each do |w|
33
51
  @worktrees[w[0]] = Git::Worktree.new(@base, w[0], w[1])
34
52
  end
35
53
  end