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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +1 -1
- data/UPGRADING.md +320 -22
- data/lib/git/branch.rb +9 -5
- data/lib/git/object.rb +56 -8
- data/lib/git/parsers/status.rb +251 -0
- data/lib/git/parsers/worktree.rb +185 -0
- data/lib/git/repository/object_operations.rb +257 -20
- data/lib/git/repository/stashing.rb +532 -52
- data/lib/git/repository/status_operations.rb +56 -8
- data/lib/git/repository/worktree_operations.rb +198 -18
- data/lib/git/stash.rb +31 -2
- data/lib/git/stashes.rb +47 -11
- data/lib/git/status.rb +14 -0
- data/lib/git/status_file_info.rb +258 -0
- data/lib/git/status_info.rb +189 -0
- data/lib/git/tag_info.rb +2 -1
- data/lib/git/version.rb +1 -1
- data/lib/git/worktree.rb +39 -0
- data/lib/git/worktree_info.rb +128 -0
- data/lib/git/worktrees.rb +19 -1
- data/lib/git.rb +4 -0
- metadata +8 -3
|
@@ -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::
|
|
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
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
|
-
|
|
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
|