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