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
@@ -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
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Represents a tag that failed to be deleted
5
+ #
6
+ # This is an immutable data object returned as part of {Git::TagDeleteResult}
7
+ # when one or more tags could not be deleted.
8
+ #
9
+ # @example
10
+ # failure = Git::TagDeleteFailure.new(
11
+ # name: 'nonexistent',
12
+ # error_message: "tag 'nonexistent' not found."
13
+ # )
14
+ # failure.name #=> 'nonexistent'
15
+ # failure.error_message #=> "tag 'nonexistent' not found."
16
+ #
17
+ # @see Git::TagDeleteResult
18
+ #
19
+ # @see Git::Commands::Tag::Delete
20
+ #
21
+ # @api public
22
+ #
23
+ # @!attribute [r] name
24
+ # The name of the tag that failed to be deleted
25
+ #
26
+ # @return [String]
27
+ #
28
+ # @!attribute [r] error_message
29
+ # The error message from git explaining why the tag could not be deleted
30
+ #
31
+ # @return [String]
32
+ #
33
+ TagDeleteFailure = Data.define(:name, :error_message)
34
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/tag_info'
4
+ require 'git/tag_delete_failure'
5
+
6
+ module Git
7
+ # Represents the result of a tag delete operation
8
+ #
9
+ # This is an immutable data object returned by {Git::Commands::Tag::Delete#call}.
10
+ # It contains information about which tags were successfully deleted and which
11
+ # failed to be deleted, along with the reason for each failure.
12
+ #
13
+ # Git's `git tag -d` command uses "best effort" semantics - it deletes as many
14
+ # tags as possible and reports errors for those that couldn't be deleted. This
15
+ # result object reflects that behavior, allowing callers to inspect both
16
+ # successes and failures.
17
+ #
18
+ # @example Successful deletion of all tags
19
+ # result = tag_delete.call('v1.0.0', 'v2.0.0')
20
+ # result.success? #=> true
21
+ # result.deleted.map(&:name) #=> ['v1.0.0', 'v2.0.0']
22
+ # result.not_deleted #=> []
23
+ #
24
+ # @example Partial failure (some tags deleted, some not found)
25
+ # result = tag_delete.call('v1.0.0', 'nonexistent', 'v2.0.0')
26
+ # result.success? #=> false
27
+ # result.deleted.map(&:name) #=> ['v1.0.0', 'v2.0.0']
28
+ # result.not_deleted.first.name #=> 'nonexistent'
29
+ # result.not_deleted.first.error_message #=> "tag 'nonexistent' not found."
30
+ #
31
+ # @see Git::TagInfo
32
+ #
33
+ # @see Git::TagDeleteFailure
34
+ #
35
+ # @see Git::Commands::Tag::Delete
36
+ #
37
+ # @api public
38
+ #
39
+ # @!attribute [r] deleted
40
+ # Tags that were successfully deleted
41
+ #
42
+ # @return [Array<Git::TagInfo>]
43
+ #
44
+ # @!attribute [r] not_deleted
45
+ # Tags that could not be deleted, with the reason for each failure
46
+ #
47
+ # @return [Array<Git::TagDeleteFailure>]
48
+ #
49
+ TagDeleteResult = Data.define(:deleted, :not_deleted) do
50
+ # Returns true if all requested tags were successfully deleted
51
+ #
52
+ # @example Check if the delete operation succeeded
53
+ # result = tag_delete.call('v1.0.0')
54
+ # if result.success?
55
+ # puts "All tags deleted successfully"
56
+ # else
57
+ # puts "Some tags could not be deleted:"
58
+ # result.not_deleted.each { |f| puts " #{f.name}: #{f.error_message}" }
59
+ # end
60
+ #
61
+ # @return [Boolean] true if no tags failed to delete, false otherwise
62
+ def success?
63
+ not_deleted.empty?
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ require 'git/author_info'
6
+
7
+ module Git
8
+ # Value object representing tag metadata from git tag output
9
+ #
10
+ # This is a lightweight, immutable data structure returned by tag listing
11
+ # commands. It contains only the data parsed from git output without any
12
+ # repository context or operations.
13
+ #
14
+ # The tagger identity is a nested {Git::AuthorInfo}. Its `date` is a `Time`
15
+ # parsed from git's strict ISO 8601 output, not an ISO 8601 string.
16
+ #
17
+ # @example Annotated tag
18
+ # info = Git::TagInfo.new(
19
+ # name: 'v1.0.0',
20
+ # oid: 'abc123def456', # tag object's ID
21
+ # target_oid: 'def456abc789', # commit it points to
22
+ # objecttype: 'tag',
23
+ # tagger: Git::AuthorInfo.new(
24
+ # name: 'John Doe',
25
+ # email: 'john@example.com',
26
+ # date: Time.iso8601('2024-01-15T10:30:00-08:00')
27
+ # ),
28
+ # message: 'Release version 1.0.0'
29
+ # )
30
+ # info.annotated? #=> true
31
+ # info.tagger.name #=> 'John Doe'
32
+ # info.tagger.date #=> 2024-01-15 10:30:00 -0800
33
+ #
34
+ # @example Lightweight tag
35
+ # info = Git::TagInfo.new(
36
+ # name: 'v1.0.0',
37
+ # oid: nil, # no tag object exists
38
+ # target_oid: 'def456abc789', # commit ID
39
+ # objecttype: 'commit',
40
+ # tagger: nil,
41
+ # message: nil
42
+ # )
43
+ # info.lightweight? #=> true
44
+ # info.tagger #=> nil
45
+ #
46
+ # @see Git::Repository::ObjectOperations#tag_list for the repository method
47
+ # that returns these
48
+ #
49
+ # @see Git::Commands::Tag::List for the command that produces these
50
+ #
51
+ # @see Git::AuthorInfo for the nested tagger identity
52
+ #
53
+ # @api public
54
+ #
55
+ # @!attribute [r] name
56
+ # @return [String] the tag name (e.g., 'v1.0.0')
57
+ #
58
+ # @!attribute [r] oid
59
+ # The object ID of the tag object itself.
60
+ #
61
+ # For annotated tags, this is the tag object's ID. For lightweight tags,
62
+ # this is nil because lightweight tags are not objects in the git database.
63
+ #
64
+ # @return [String, nil] the tag object's ID, or nil for lightweight tags
65
+ #
66
+ # @!attribute [r] target_oid
67
+ # The object ID of the commit this tag ultimately points to.
68
+ #
69
+ # For both annotated and lightweight tags, this is the commit ID that the
70
+ # tag resolves to (i.e., the dereferenced target).
71
+ #
72
+ # @return [String] the commit ID this tag points to
73
+ #
74
+ # @!attribute [r] objecttype
75
+ # @return [String] 'tag' for annotated tags, 'commit' for lightweight tags
76
+ #
77
+ # @!attribute [r] tagger
78
+ # The identity of the person who created the tag object.
79
+ #
80
+ # Lightweight tags have no tag object and therefore no tagger. The nested
81
+ # `email` has no angle brackets and the nested `date` is a `Time`.
82
+ #
83
+ # @return [Git::AuthorInfo, nil] the tagger, or nil for lightweight tags
84
+ #
85
+ # @!attribute [r] message
86
+ # @return [String, nil] the tag message, or nil for lightweight tags
87
+ #
88
+ TagInfo = Data.define(:name, :oid, :target_oid, :objecttype, :tagger, :message) do
89
+ # @return [Boolean] true if this is an annotated tag (oid is present)
90
+ def annotated?
91
+ !oid.nil?
92
+ end
93
+
94
+ # @return [Boolean] true if this is a lightweight tag (oid is nil)
95
+ def lightweight?
96
+ oid.nil?
97
+ end
98
+ end
99
+ end