git 1.19.1 → 5.5.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.
Files changed (265) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -1
  3. data/AI_POLICY.md +24 -0
  4. data/CHANGELOG.md +689 -0
  5. data/CODE_OF_CONDUCT.md +25 -0
  6. data/CONTRIBUTING.md +1175 -97
  7. data/GOVERNANCE.md +106 -0
  8. data/LICENSE +1 -1
  9. data/MAINTAINERS.md +17 -4
  10. data/README.md +476 -320
  11. data/UPGRADING.md +1138 -0
  12. data/git.gemspec +124 -36
  13. data/lib/git/author.rb +39 -7
  14. data/lib/git/author_info.rb +66 -0
  15. data/lib/git/branch.rb +615 -65
  16. data/lib/git/branch_delete_failure.rb +34 -0
  17. data/lib/git/branch_delete_result.rb +66 -0
  18. data/lib/git/branch_info.rb +237 -0
  19. data/lib/git/branches.rb +167 -44
  20. data/lib/git/command_line/base.rb +247 -0
  21. data/lib/git/command_line/capturing.rb +308 -0
  22. data/lib/git/command_line/result.rb +88 -0
  23. data/lib/git/command_line/streaming.rb +236 -0
  24. data/lib/git/command_line.rb +52 -0
  25. data/lib/git/commands/add.rb +139 -0
  26. data/lib/git/commands/am/abort.rb +43 -0
  27. data/lib/git/commands/am/apply.rb +263 -0
  28. data/lib/git/commands/am/continue.rb +43 -0
  29. data/lib/git/commands/am/quit.rb +43 -0
  30. data/lib/git/commands/am/retry.rb +49 -0
  31. data/lib/git/commands/am/show_current_patch.rb +64 -0
  32. data/lib/git/commands/am/skip.rb +42 -0
  33. data/lib/git/commands/am.rb +33 -0
  34. data/lib/git/commands/apply.rb +242 -0
  35. data/lib/git/commands/archive/list_formats.rb +46 -0
  36. data/lib/git/commands/archive.rb +145 -0
  37. data/lib/git/commands/arguments.rb +4521 -0
  38. data/lib/git/commands/base.rb +502 -0
  39. data/lib/git/commands/branch/copy.rb +102 -0
  40. data/lib/git/commands/branch/create.rb +177 -0
  41. data/lib/git/commands/branch/delete.rb +88 -0
  42. data/lib/git/commands/branch/list.rb +178 -0
  43. data/lib/git/commands/branch/move.rb +102 -0
  44. data/lib/git/commands/branch/set_upstream.rb +86 -0
  45. data/lib/git/commands/branch/show_current.rb +49 -0
  46. data/lib/git/commands/branch/unset_upstream.rb +53 -0
  47. data/lib/git/commands/branch.rb +34 -0
  48. data/lib/git/commands/cat_file/batch.rb +385 -0
  49. data/lib/git/commands/cat_file/filtered.rb +105 -0
  50. data/lib/git/commands/cat_file/raw.rb +271 -0
  51. data/lib/git/commands/cat_file.rb +49 -0
  52. data/lib/git/commands/checkout/branch.rb +153 -0
  53. data/lib/git/commands/checkout/files.rb +116 -0
  54. data/lib/git/commands/checkout.rb +38 -0
  55. data/lib/git/commands/checkout_index.rb +106 -0
  56. data/lib/git/commands/clean.rb +102 -0
  57. data/lib/git/commands/clone.rb +241 -0
  58. data/lib/git/commands/commit.rb +273 -0
  59. data/lib/git/commands/commit_tree.rb +101 -0
  60. data/lib/git/commands/config_option_syntax/add.rb +86 -0
  61. data/lib/git/commands/config_option_syntax/get.rb +121 -0
  62. data/lib/git/commands/config_option_syntax/get_all.rb +118 -0
  63. data/lib/git/commands/config_option_syntax/get_color.rb +95 -0
  64. data/lib/git/commands/config_option_syntax/get_color_bool.rb +96 -0
  65. data/lib/git/commands/config_option_syntax/get_regexp.rb +119 -0
  66. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +111 -0
  67. data/lib/git/commands/config_option_syntax/list.rb +111 -0
  68. data/lib/git/commands/config_option_syntax/remove_section.rb +79 -0
  69. data/lib/git/commands/config_option_syntax/rename_section.rb +83 -0
  70. data/lib/git/commands/config_option_syntax/replace_all.rb +109 -0
  71. data/lib/git/commands/config_option_syntax/set.rb +119 -0
  72. data/lib/git/commands/config_option_syntax/unset.rb +92 -0
  73. data/lib/git/commands/config_option_syntax/unset_all.rb +94 -0
  74. data/lib/git/commands/config_option_syntax.rb +56 -0
  75. data/lib/git/commands/describe.rb +156 -0
  76. data/lib/git/commands/diff.rb +657 -0
  77. data/lib/git/commands/diff_files.rb +519 -0
  78. data/lib/git/commands/diff_index.rb +499 -0
  79. data/lib/git/commands/fetch.rb +354 -0
  80. data/lib/git/commands/fsck.rb +138 -0
  81. data/lib/git/commands/gc.rb +134 -0
  82. data/lib/git/commands/grep.rb +339 -0
  83. data/lib/git/commands/init.rb +101 -0
  84. data/lib/git/commands/log.rb +634 -0
  85. data/lib/git/commands/ls_files.rb +195 -0
  86. data/lib/git/commands/ls_remote.rb +161 -0
  87. data/lib/git/commands/ls_tree.rb +135 -0
  88. data/lib/git/commands/maintenance/register.rb +77 -0
  89. data/lib/git/commands/maintenance/run.rb +109 -0
  90. data/lib/git/commands/maintenance/start.rb +71 -0
  91. data/lib/git/commands/maintenance/stop.rb +60 -0
  92. data/lib/git/commands/maintenance/unregister.rb +84 -0
  93. data/lib/git/commands/maintenance.rb +31 -0
  94. data/lib/git/commands/merge/abort.rb +44 -0
  95. data/lib/git/commands/merge/continue.rb +44 -0
  96. data/lib/git/commands/merge/quit.rb +46 -0
  97. data/lib/git/commands/merge/start.rb +250 -0
  98. data/lib/git/commands/merge.rb +28 -0
  99. data/lib/git/commands/merge_base.rb +91 -0
  100. data/lib/git/commands/mv.rb +82 -0
  101. data/lib/git/commands/name_rev.rb +119 -0
  102. data/lib/git/commands/pull.rb +382 -0
  103. data/lib/git/commands/push.rb +251 -0
  104. data/lib/git/commands/read_tree.rb +154 -0
  105. data/lib/git/commands/remote/add.rb +96 -0
  106. data/lib/git/commands/remote/get_url.rb +68 -0
  107. data/lib/git/commands/remote/list.rb +56 -0
  108. data/lib/git/commands/remote/prune.rb +63 -0
  109. data/lib/git/commands/remote/remove.rb +52 -0
  110. data/lib/git/commands/remote/rename.rb +76 -0
  111. data/lib/git/commands/remote/set_branches.rb +70 -0
  112. data/lib/git/commands/remote/set_head.rb +89 -0
  113. data/lib/git/commands/remote/set_url.rb +78 -0
  114. data/lib/git/commands/remote/set_url_add.rb +70 -0
  115. data/lib/git/commands/remote/set_url_delete.rb +71 -0
  116. data/lib/git/commands/remote/show.rb +77 -0
  117. data/lib/git/commands/remote/update.rb +79 -0
  118. data/lib/git/commands/remote.rb +42 -0
  119. data/lib/git/commands/repack.rb +281 -0
  120. data/lib/git/commands/reset.rb +154 -0
  121. data/lib/git/commands/rev_parse.rb +304 -0
  122. data/lib/git/commands/revert/abort.rb +45 -0
  123. data/lib/git/commands/revert/continue.rb +62 -0
  124. data/lib/git/commands/revert/quit.rb +47 -0
  125. data/lib/git/commands/revert/skip.rb +44 -0
  126. data/lib/git/commands/revert/start.rb +158 -0
  127. data/lib/git/commands/revert.rb +29 -0
  128. data/lib/git/commands/rm.rb +113 -0
  129. data/lib/git/commands/show.rb +632 -0
  130. data/lib/git/commands/show_ref/exclude_existing.rb +119 -0
  131. data/lib/git/commands/show_ref/exists.rb +80 -0
  132. data/lib/git/commands/show_ref/list.rb +149 -0
  133. data/lib/git/commands/show_ref/verify.rb +122 -0
  134. data/lib/git/commands/show_ref.rb +42 -0
  135. data/lib/git/commands/stash/apply.rb +81 -0
  136. data/lib/git/commands/stash/branch.rb +67 -0
  137. data/lib/git/commands/stash/clear.rb +43 -0
  138. data/lib/git/commands/stash/create.rb +60 -0
  139. data/lib/git/commands/stash/drop.rb +73 -0
  140. data/lib/git/commands/stash/list.rb +43 -0
  141. data/lib/git/commands/stash/pop.rb +87 -0
  142. data/lib/git/commands/stash/push.rb +112 -0
  143. data/lib/git/commands/stash/show.rb +158 -0
  144. data/lib/git/commands/stash/store.rb +72 -0
  145. data/lib/git/commands/stash.rb +38 -0
  146. data/lib/git/commands/status.rb +174 -0
  147. data/lib/git/commands/symbolic_ref/delete.rb +72 -0
  148. data/lib/git/commands/symbolic_ref/read.rb +99 -0
  149. data/lib/git/commands/symbolic_ref/update.rb +79 -0
  150. data/lib/git/commands/symbolic_ref.rb +38 -0
  151. data/lib/git/commands/tag/create.rb +142 -0
  152. data/lib/git/commands/tag/delete.rb +57 -0
  153. data/lib/git/commands/tag/list.rb +146 -0
  154. data/lib/git/commands/tag/verify.rb +71 -0
  155. data/lib/git/commands/tag.rb +26 -0
  156. data/lib/git/commands/update_ref/batch.rb +145 -0
  157. data/lib/git/commands/update_ref/delete.rb +90 -0
  158. data/lib/git/commands/update_ref/update.rb +103 -0
  159. data/lib/git/commands/update_ref.rb +42 -0
  160. data/lib/git/commands/version.rb +60 -0
  161. data/lib/git/commands/worktree/add.rb +139 -0
  162. data/lib/git/commands/worktree/list.rb +64 -0
  163. data/lib/git/commands/worktree/lock.rb +58 -0
  164. data/lib/git/commands/worktree/management_base.rb +51 -0
  165. data/lib/git/commands/worktree/move.rb +66 -0
  166. data/lib/git/commands/worktree/prune.rb +67 -0
  167. data/lib/git/commands/worktree/remove.rb +63 -0
  168. data/lib/git/commands/worktree/repair.rb +76 -0
  169. data/lib/git/commands/worktree/unlock.rb +47 -0
  170. data/lib/git/commands/worktree.rb +43 -0
  171. data/lib/git/commands/write_tree.rb +68 -0
  172. data/lib/git/commands.rb +88 -0
  173. data/lib/git/config.rb +72 -5
  174. data/lib/git/config_entry_info.rb +106 -0
  175. data/lib/git/configuring.rb +795 -0
  176. data/lib/git/detached_head_info.rb +57 -0
  177. data/lib/git/diff.rb +437 -86
  178. data/lib/git/diff_file_numstat_info.rb +31 -0
  179. data/lib/git/diff_file_patch_info.rb +136 -0
  180. data/lib/git/diff_file_raw_info.rb +129 -0
  181. data/lib/git/diff_info.rb +162 -0
  182. data/lib/git/diff_path_status.rb +107 -0
  183. data/lib/git/diff_result.rb +34 -0
  184. data/lib/git/diff_stats.rb +111 -0
  185. data/lib/git/dirstat_info.rb +102 -0
  186. data/lib/git/encoding_utils.rb +32 -1
  187. data/lib/git/errors.rb +285 -0
  188. data/lib/git/escaped_path.rb +57 -5
  189. data/lib/git/execution_context/global.rb +31 -0
  190. data/lib/git/execution_context/repository.rb +151 -0
  191. data/lib/git/execution_context.rb +559 -0
  192. data/lib/git/factories.rb +813 -0
  193. data/lib/git/file_ref.rb +77 -0
  194. data/lib/git/fsck_object.rb +56 -0
  195. data/lib/git/fsck_result.rb +132 -0
  196. data/lib/git/log.rb +306 -90
  197. data/lib/git/object.rb +563 -141
  198. data/lib/git/parsers/branch.rb +240 -0
  199. data/lib/git/parsers/cat_file.rb +111 -0
  200. data/lib/git/parsers/config_entry.rb +110 -0
  201. data/lib/git/parsers/diff.rb +792 -0
  202. data/lib/git/parsers/fsck.rb +144 -0
  203. data/lib/git/parsers/grep.rb +42 -0
  204. data/lib/git/parsers/ls_remote.rb +79 -0
  205. data/lib/git/parsers/ls_tree.rb +58 -0
  206. data/lib/git/parsers/remote.rb +162 -0
  207. data/lib/git/parsers/stash.rb +292 -0
  208. data/lib/git/parsers/status.rb +251 -0
  209. data/lib/git/parsers/tag.rb +341 -0
  210. data/lib/git/parsers/worktree.rb +185 -0
  211. data/lib/git/path_resolver.rb +206 -0
  212. data/lib/git/remote.rb +165 -12
  213. data/lib/git/remote_info.rb +203 -0
  214. data/lib/git/repository/branching.rb +964 -0
  215. data/lib/git/repository/committing.rb +246 -0
  216. data/lib/git/repository/context_helpers.rb +293 -0
  217. data/lib/git/repository/diffing.rb +785 -0
  218. data/lib/git/repository/inspecting.rb +252 -0
  219. data/lib/git/repository/logging.rb +410 -0
  220. data/lib/git/repository/maintenance.rb +65 -0
  221. data/lib/git/repository/merging.rb +451 -0
  222. data/lib/git/repository/object_operations.rb +1551 -0
  223. data/lib/git/repository/remote_operations.rb +984 -0
  224. data/lib/git/repository/shared_private.rb +120 -0
  225. data/lib/git/repository/staging.rb +587 -0
  226. data/lib/git/repository/stashing.rb +623 -0
  227. data/lib/git/repository/status_operations.rb +249 -0
  228. data/lib/git/repository/worktree_operations.rb +339 -0
  229. data/lib/git/repository.rb +484 -2
  230. data/lib/git/stash.rb +109 -12
  231. data/lib/git/stash_info.rb +102 -0
  232. data/lib/git/stashes.rb +169 -26
  233. data/lib/git/status.rb +308 -122
  234. data/lib/git/status_file_info.rb +258 -0
  235. data/lib/git/status_info.rb +189 -0
  236. data/lib/git/tag_delete_failure.rb +34 -0
  237. data/lib/git/tag_delete_result.rb +66 -0
  238. data/lib/git/tag_info.rb +99 -0
  239. data/lib/git/url.rb +15 -8
  240. data/lib/git/version.rb +113 -2
  241. data/lib/git/version_constraint.rb +85 -0
  242. data/lib/git/worktree.rb +150 -8
  243. data/lib/git/worktree_info.rb +128 -0
  244. data/lib/git/worktrees.rb +118 -13
  245. data/lib/git.rb +632 -234
  246. metadata +369 -54
  247. data/.github/stale.yml +0 -25
  248. data/.github/workflows/continuous_integration.yml +0 -49
  249. data/.gitignore +0 -10
  250. data/Dockerfile.changelog-rs +0 -12
  251. data/Gemfile +0 -5
  252. data/ISSUE_TEMPLATE.md +0 -15
  253. data/PULL_REQUEST_TEMPLATE.md +0 -9
  254. data/RELEASING.md +0 -70
  255. data/Rakefile +0 -60
  256. data/lib/git/base/factory.rb +0 -99
  257. data/lib/git/base.rb +0 -711
  258. data/lib/git/command_line_result.rb +0 -86
  259. data/lib/git/failed_error.rb +0 -53
  260. data/lib/git/git_execute_error.rb +0 -7
  261. data/lib/git/index.rb +0 -5
  262. data/lib/git/lib.rb +0 -1328
  263. data/lib/git/path.rb +0 -31
  264. data/lib/git/signaled_error.rb +0 -50
  265. data/lib/git/working_directory.rb +0 -4
data/lib/git/status.rb CHANGED
@@ -1,108 +1,146 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Git
4
+ # The Status class gets the status of a git repository. It identifies which
5
+ # files have been modified, added, or deleted, including untracked files.
6
+ # The Status object is an Enumerable of StatusFile objects.
7
+ #
8
+ # @example Inspect repository status
9
+ # repo = Git.open('/path/to/repo')
10
+ # status = repo.status
11
+ # status.changed.each { |path, _file| puts "Modified: #{path}" }
12
+ # status.added.each { |path, _file| puts "Added: #{path}" }
13
+ # status.deleted.each { |path, _file| puts "Deleted: #{path}" }
14
+ # status.untracked.each { |path, _file| puts "Untracked: #{path}" }
2
15
  #
3
- # A class for git status
16
+ # @deprecated Use {Git::StatusInfo}, returned by {Git::Repository#status_info},
17
+ # instead; this class will be removed in v6.0.0
18
+ #
19
+ # @api public
4
20
  #
5
21
  class Status
6
22
  include Enumerable
7
23
 
24
+ # Create a new Status for the given repository
25
+ #
26
+ # @param base [Git::Repository] the git object backing this status
27
+ #
28
+ # @deprecated Use {Git::Repository#status_info} instead
29
+ #
8
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
9
37
  @base = base
10
- construct_status
38
+ # The factory returns a hash of file paths to StatusFile objects.
39
+ @files = StatusFileFactory.new(base).construct_files
11
40
  end
12
41
 
42
+ # Return files modified in the index and/or working tree
13
43
  #
14
- # Returns an Enumerable containing files that have changed from the
15
- # git base directory
44
+ # Includes both staged modifications (index vs HEAD) and unstaged modifications
45
+ # (working tree vs index).
16
46
  #
17
- # @return [Enumerable]
18
- def changed
19
- @files.select { |_k, f| f.type == 'M' }
20
- end
47
+ # @return [Hash{String => Git::Status::StatusFile}] changed files keyed by path
48
+ #
49
+ def changed = @changed ||= select_files { |f| f.type == 'M' }
21
50
 
51
+ # Return files added to the index that are not yet in HEAD
22
52
  #
23
- # Determines whether the given file has been changed.
24
- # File path starts at git base directory
53
+ # @return [Hash{String => Git::Status::StatusFile}] added files keyed by path
25
54
  #
26
- # @param file [String] The name of the file.
27
- # @example Check if lib/git.rb has changed.
28
- # changed?('lib/git.rb')
29
- # @return [Boolean]
30
- def changed?(file)
31
- changed.member?(file)
32
- end
55
+ def added = @added ||= select_files { |f| f.type == 'A' }
33
56
 
57
+ # Return files deleted from the index
34
58
  #
35
- # Returns an Enumerable containing files that have been added.
36
- # File path starts at git base directory
59
+ # @return [Hash{String => Git::Status::StatusFile}] deleted files keyed by path
37
60
  #
38
- # @return [Enumerable]
39
- def added
40
- @files.select { |_k, f| f.type == 'A' }
41
- end
61
+ def deleted = @deleted ||= select_files { |f| f.type == 'D' }
42
62
 
63
+ # Return files present in the working tree but not tracked by git
43
64
  #
44
- # Determines whether the given file has been added to the repository
45
- # File path starts at git base directory
65
+ # @return [Hash{String => Git::Status::StatusFile}] untracked files keyed by path
46
66
  #
47
- # @param file [String] The name of the file.
48
- # @example Check if lib/git.rb is added.
49
- # added?('lib/git.rb')
50
- # @return [Boolean]
51
- def added?(file)
52
- added.member?(file)
53
- end
67
+ def untracked = @untracked ||= select_files(&:untracked)
54
68
 
69
+ # Return `true` if `file` has been modified in the index or working tree
55
70
  #
56
- # Returns an Enumerable containing files that have been deleted.
57
- # File path starts at git base directory
71
+ # @param file [String] the repository-relative path to check
58
72
  #
59
- # @return [Enumerable]
60
- def deleted
61
- @files.select { |_k, f| f.type == 'D' }
62
- end
73
+ # @return [Boolean] `true` if the file has been modified
74
+ #
75
+ def changed?(file) = file_in_collection?(:changed, file)
63
76
 
77
+ # Return `true` if `file` has been added to the index
64
78
  #
65
- # Determines whether the given file has been deleted from the repository
66
- # File path starts at git base directory
79
+ # @param file [String] the repository-relative path to check
67
80
  #
68
- # @param file [String] The name of the file.
69
- # @example Check if lib/git.rb is deleted.
70
- # deleted?('lib/git.rb')
71
- # @return [Boolean]
72
- def deleted?(file)
73
- deleted.member?(file)
74
- end
81
+ # @return [Boolean] `true` if the file has been added
82
+ #
83
+ def added?(file) = file_in_collection?(:added, file)
75
84
 
85
+ # Return `true` if `file` has been deleted from the index
76
86
  #
77
- # Returns an Enumerable containing files that are not tracked in git.
78
- # File path starts at git base directory
87
+ # @param file [String] the repository-relative path to check
79
88
  #
80
- # @return [Enumerable]
81
- def untracked
82
- @files.select { |_k, f| f.untracked }
83
- end
89
+ # @return [Boolean] `true` if the file has been deleted
90
+ #
91
+ def deleted?(file) = file_in_collection?(:deleted, file)
84
92
 
93
+ # Return `true` if `file` is not tracked by git
85
94
  #
86
- # Determines whether the given file has is tracked by git.
87
- # File path starts at git base directory
95
+ # @param file [String] the repository-relative path to check
88
96
  #
89
- # @param file [String] The name of the file.
90
- # @example Check if lib/git.rb is an untracked file.
91
- # untracked?('lib/git.rb')
92
- # @return [Boolean]
93
- def untracked?(file)
94
- untracked.member?(file)
95
- end
97
+ # @return [Boolean] `true` if the file is untracked
98
+ #
99
+ def untracked?(file) = file_in_collection?(:untracked, file)
100
+
101
+ # Return the {Git::Status::StatusFile} for the given path
102
+ #
103
+ # @param file [String] the repository-relative path
104
+ #
105
+ # @return [Git::Status::StatusFile, nil] the status file, or `nil` if not found
106
+ #
107
+ def [](file) = @files[file]
96
108
 
109
+ # Iterate over all status files
110
+ #
111
+ # @overload each
112
+ #
113
+ # @return [Enumerator<Git::Status::StatusFile>] an enumerator over all status files
114
+ #
115
+ # @overload each(&block)
116
+ #
117
+ # @return [Array<Git::Status::StatusFile>] the full list of status files
118
+ #
119
+ # @yield [file] each {Git::Status::StatusFile} in the repository
120
+ #
121
+ # @yieldparam file [Git::Status::StatusFile] a single file's status
122
+ #
123
+ # @yieldreturn [void]
124
+ #
125
+ def each(&) = @files.values.each(&)
126
+
127
+ # Return a formatted multi-line string representation of the status
128
+ #
129
+ # @return [String] one indented block per file showing its SHA, mode, type,
130
+ # stage, and untracked flag
131
+ #
97
132
  def pretty
98
- out = ''
99
- each do |file|
100
- out << pretty_file(file)
101
- end
102
- out << "\n"
103
- out
133
+ map { |file| pretty_file(file) }.join << "\n"
104
134
  end
105
135
 
136
+ private
137
+
138
+ # Format a single file's status as an indented multi-line string
139
+ #
140
+ # @param file [Git::Status::StatusFile] the file to format
141
+ #
142
+ # @return [String] the formatted status block for this file
143
+ #
106
144
  def pretty_file(file)
107
145
  <<~FILE
108
146
  #{file.path}
@@ -114,84 +152,232 @@ module Git
114
152
  FILE
115
153
  end
116
154
 
117
- # enumerable method
155
+ # Return a hash of files for which the block returns a truthy value
156
+ #
157
+ # @return [Hash{String => Git::Status::StatusFile}] matching files keyed by path
158
+ #
159
+ # @yield [file] each {Git::Status::StatusFile} in the repository
160
+ #
161
+ # @yieldparam file [Git::Status::StatusFile] a single file's status
162
+ #
163
+ # @yieldreturn [Boolean] truthy to include the file in the result
164
+ #
165
+ def select_files(&block)
166
+ @files.select { |_path, file| block.call(file) }
167
+ end
118
168
 
119
- def [](file)
120
- @files[file]
169
+ # Return `true` if `file_path` exists in the named status collection
170
+ #
171
+ # @param collection_name [Symbol] the collection to check (e.g. `:changed`)
172
+ #
173
+ # @param file_path [String] the repository-relative path to look up
174
+ #
175
+ # @return [Boolean] `true` if the path is present in the collection
176
+ #
177
+ def file_in_collection?(collection_name, file_path)
178
+ collection = public_send(collection_name)
179
+ if ignore_case?
180
+ downcased_keys(collection_name).include?(file_path.downcase)
181
+ else
182
+ collection.key?(file_path)
183
+ end
184
+ end
185
+
186
+ # Return a memoized set of downcased keys for the named collection
187
+ #
188
+ # @param collection_name [Symbol] the collection whose keys to downcase
189
+ #
190
+ # @return [Set<String>] the lowercased path keys
191
+ #
192
+ def downcased_keys(collection_name)
193
+ @_downcased_keys ||= {}
194
+ @_downcased_keys[collection_name] ||=
195
+ public_send(collection_name).keys.to_set(&:downcase)
121
196
  end
122
197
 
123
- def each(&block)
124
- @files.values.each(&block)
198
+ # Return `true` when git is configured to ignore filename case
199
+ #
200
+ # Reads `core.ignoreCase` with {Git::Repository#config_get}. Returns `false`
201
+ # when the config value is absent.
202
+ #
203
+ # @return [Boolean] `true` when `core.ignoreCase` is `"true"`
204
+ #
205
+ def ignore_case?
206
+ return @ignore_case if defined?(@ignore_case)
207
+
208
+ @ignore_case = (@base.config_get('core.ignoreCase')&.value == 'true')
125
209
  end
210
+ end
211
+ end
126
212
 
127
- # subclass that does heavy lifting
213
+ module Git
214
+ class Status
215
+ # Represents a single file's status in the git repository. Each instance
216
+ # holds information about a file's state in the index and working tree.
217
+ #
218
+ # @deprecated Use {Git::StatusFileInfo}, held by {Git::StatusInfo#files},
219
+ # instead; this class will be removed in v6.0.0
220
+ #
221
+ # @api public
222
+ #
128
223
  class StatusFile
129
- attr_accessor :path, :type, :stage, :untracked
130
- attr_accessor :mode_index, :mode_repo
131
- attr_accessor :sha_index, :sha_repo
224
+ # The repository-relative file path
225
+ #
226
+ # @return [String] the path
227
+ attr_reader :path
228
+
229
+ # The change type for this file
230
+ #
231
+ # @return [String, nil] `"M"` for modified, `"A"` for added, `"D"` for deleted,
232
+ # or `nil` when not applicable
233
+ attr_reader :type
234
+
235
+ # The merge stage for this file
236
+ #
237
+ # @return [String, nil] `"0"` for normal entries, or a non-zero value during
238
+ # a merge conflict
239
+ attr_reader :stage
240
+
241
+ # The file mode recorded in the index
242
+ #
243
+ # @return [String, nil] the octal file mode (e.g. `"100644"`), or `nil`
244
+ attr_reader :mode_index
245
+
246
+ # The file mode recorded in HEAD
247
+ #
248
+ # @return [String, nil] the octal file mode (e.g. `"100644"`), or `nil`
249
+ attr_reader :mode_repo
250
+
251
+ # The SHA of the index version of this file
252
+ #
253
+ # @return [String, nil] the SHA-1 hex digest, or `nil` if unavailable
254
+ attr_reader :sha_index
132
255
 
256
+ # The SHA of the HEAD version of this file
257
+ #
258
+ # @return [String, nil] the SHA-1 hex digest, or `nil` if unavailable
259
+ attr_reader :sha_repo
260
+
261
+ # Whether this file is untracked
262
+ #
263
+ # @return [Boolean, nil] `true` when the file is not tracked by git
264
+ attr_reader :untracked
265
+
266
+ # Initialize a new StatusFile with the given git object and data hash
267
+ #
268
+ # @param base [Git::Repository] the git object
269
+ #
270
+ # @param hash [Hash] raw status data for this file
271
+ #
133
272
  def initialize(base, hash)
134
- @base = base
135
- @path = hash[:path]
136
- @type = hash[:type]
137
- @stage = hash[:stage]
273
+ @base = base
274
+ @path = hash[:path]
275
+ @type = hash[:type]
276
+ @stage = hash[:stage]
138
277
  @mode_index = hash[:mode_index]
139
- @mode_repo = hash[:mode_repo]
140
- @sha_index = hash[:sha_index]
141
- @sha_repo = hash[:sha_repo]
142
- @untracked = hash[:untracked]
278
+ @mode_repo = hash[:mode_repo]
279
+ @sha_index = hash[:sha_index]
280
+ @sha_repo = hash[:sha_repo]
281
+ @untracked = hash[:untracked]
143
282
  end
144
283
 
284
+ # Return a blob object for the index or repo version of this file
285
+ #
286
+ # @param type [Symbol] `:index` (default) for the index version, or
287
+ # `:repo` for the HEAD version
288
+ #
289
+ # @return [Git::Object::Blob, nil] the blob object, or `nil` if no SHA
290
+ # is available for the requested version
291
+ #
145
292
  def blob(type = :index)
146
- if type == :repo
147
- @base.object(@sha_repo)
148
- else
149
- begin
150
- @base.object(@sha_index)
151
- rescue
152
- @base.object(@sha_repo)
153
- end
154
- end
293
+ sha = type == :repo ? sha_repo : (sha_index || sha_repo)
294
+ @base.object(sha) if sha
155
295
  end
156
296
  end
297
+ end
298
+ end
157
299
 
158
- private
159
-
160
- def construct_status
161
- @files = @base.lib.ls_files
162
-
163
- fetch_untracked
164
- fetch_modified
165
- fetch_added
300
+ module Git
301
+ class Status
302
+ # A factory class responsible for fetching git status data and building
303
+ # a hash of StatusFile objects.
304
+ #
305
+ # @api private
306
+ #
307
+ class StatusFileFactory
308
+ # Create a new factory backed by the given git object
309
+ #
310
+ # @param base [Git::Repository] the git object used as the status data provider
311
+ #
312
+ def initialize(base)
313
+ @base = base
314
+ end
166
315
 
167
- @files.each do |k, file_hash|
168
- @files[k] = StatusFile.new(@base, file_hash)
316
+ # Gather all status data and build a hash of file paths to StatusFile objects
317
+ #
318
+ # @return [Hash{String => Git::Status::StatusFile}] file paths mapped to
319
+ # their status objects
320
+ #
321
+ def construct_files
322
+ files_data = fetch_all_files_data
323
+ files_data.transform_values do |data|
324
+ StatusFile.new(@base, data)
325
+ end
169
326
  end
170
- end
171
327
 
172
- def fetch_untracked
173
- ignore = @base.lib.ignored_files
328
+ private
174
329
 
175
- root_dir = @base.dir.path
176
- Dir.glob('**/*', File::FNM_DOTMATCH, base: root_dir) do |file|
177
- next if @files[file] || File.directory?(File.join(root_dir, file)) ||
178
- ignore.include?(file) || file =~ %r{^.git\/.+}
330
+ # Fetch and merge status information from multiple git commands
331
+ #
332
+ # @return [Hash{String => Hash}] raw per-file status data keyed by path
333
+ #
334
+ def fetch_all_files_data
335
+ files = @base.ls_files # Start with files tracked in the index.
336
+ merge_untracked_files(files)
337
+ merge_modified_files(files)
338
+ merge_head_diffs(files)
339
+ files
340
+ end
179
341
 
180
- @files[file] = { path: file, untracked: true }
342
+ # Merge untracked working-tree files into `files`
343
+ #
344
+ # @param files [Hash] the in-progress files hash to update in place
345
+ #
346
+ # @return [void]
347
+ #
348
+ def merge_untracked_files(files)
349
+ @base.untracked_files.each do |file|
350
+ files[file] = { path: file, untracked: true }
351
+ end
181
352
  end
182
- end
183
353
 
184
- def fetch_modified
185
- # find modified in tree
186
- @base.lib.diff_files.each do |path, data|
187
- @files[path] ? @files[path].merge!(data) : @files[path] = data
354
+ # Merge index-versus-working-tree diff data into `files`
355
+ #
356
+ # @param files [Hash] the in-progress files hash to update in place
357
+ #
358
+ # @return [void]
359
+ #
360
+ def merge_modified_files(files)
361
+ # Merge changes between the index and the working directory.
362
+ @base.diff_files.each do |path, data|
363
+ (files[path] ||= {}).merge!(data)
364
+ end
188
365
  end
189
- end
190
366
 
191
- def fetch_added
192
- # find added but not committed - new files
193
- @base.lib.diff_index('HEAD').each do |path, data|
194
- @files[path] ? @files[path].merge!(data) : @files[path] = data
367
+ # Merge HEAD-versus-index diff data into `files`, if commits exist
368
+ #
369
+ # @param files [Hash] the in-progress files hash to update in place
370
+ #
371
+ # @return [void]
372
+ #
373
+ def merge_head_diffs(files)
374
+ is_empty = @base.no_commits?
375
+ return if is_empty
376
+
377
+ # Merge changes between HEAD and the index.
378
+ @base.diff_index('HEAD').each do |path, data|
379
+ (files[path] ||= {}).merge!(data)
380
+ end
195
381
  end
196
382
  end
197
383
  end