git 4.4.0 → 5.0.0.beta.1

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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +67 -2705
  3. data/.github/pull_request_template.md +3 -1
  4. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  5. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  6. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  7. data/.github/skills/command-implementation/SKILL.md +229 -0
  8. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  9. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  10. data/.github/skills/dependency-management/SKILL.md +72 -0
  11. data/.github/skills/development-workflow/SKILL.md +506 -0
  12. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  13. data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
  14. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  15. data/.github/skills/facade-implementation/SKILL.md +260 -0
  16. data/.github/skills/facade-test-conventions/SKILL.md +380 -0
  17. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  18. data/.github/skills/make-skill-template/SKILL.md +176 -0
  19. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  20. data/.github/skills/project-context/SKILL.md +313 -0
  21. data/.github/skills/pull-request-review/SKILL.md +168 -0
  22. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  23. data/.github/skills/release-management/SKILL.md +125 -0
  24. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  25. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  26. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  27. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  28. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  29. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  30. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  31. data/.github/skills/test-debugging/SKILL.md +160 -0
  32. data/.github/skills/yard-documentation/SKILL.md +793 -0
  33. data/.github/workflows/continuous_integration.yml +3 -2
  34. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  35. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  36. data/.github/workflows/release.yml +3 -4
  37. data/.gitignore +8 -0
  38. data/.husky/pre-commit +13 -0
  39. data/.release-please-manifest.json +1 -1
  40. data/.rspec +3 -0
  41. data/.rubocop.yml +7 -3
  42. data/.rubocop_todo.yml +23 -5
  43. data/.yardopts +1 -0
  44. data/CHANGELOG.md +0 -53
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -17
  47. data/Rakefile +61 -9
  48. data/commitlint.test +4 -0
  49. data/git.gemspec +14 -8
  50. data/lib/git/args_builder.rb +0 -8
  51. data/lib/git/base.rb +488 -412
  52. data/lib/git/branch.rb +117 -59
  53. data/lib/git/branch_delete_failure.rb +31 -0
  54. data/lib/git/branch_delete_result.rb +63 -0
  55. data/lib/git/branch_info.rb +178 -0
  56. data/lib/git/branches.rb +130 -24
  57. data/lib/git/command_line/base.rb +245 -0
  58. data/lib/git/command_line/capturing.rb +249 -0
  59. data/lib/git/command_line/result.rb +96 -0
  60. data/lib/git/command_line/streaming.rb +194 -0
  61. data/lib/git/command_line.rb +43 -322
  62. data/lib/git/command_line_result.rb +4 -88
  63. data/lib/git/commands/add.rb +131 -0
  64. data/lib/git/commands/am/abort.rb +43 -0
  65. data/lib/git/commands/am/apply.rb +252 -0
  66. data/lib/git/commands/am/continue.rb +43 -0
  67. data/lib/git/commands/am/quit.rb +43 -0
  68. data/lib/git/commands/am/retry.rb +47 -0
  69. data/lib/git/commands/am/show_current_patch.rb +64 -0
  70. data/lib/git/commands/am/skip.rb +42 -0
  71. data/lib/git/commands/am.rb +33 -0
  72. data/lib/git/commands/apply.rb +237 -0
  73. data/lib/git/commands/archive/list_formats.rb +46 -0
  74. data/lib/git/commands/archive.rb +140 -0
  75. data/lib/git/commands/arguments.rb +3510 -0
  76. data/lib/git/commands/base.rb +403 -0
  77. data/lib/git/commands/branch/copy.rb +94 -0
  78. data/lib/git/commands/branch/create.rb +173 -0
  79. data/lib/git/commands/branch/delete.rb +80 -0
  80. data/lib/git/commands/branch/list.rb +162 -0
  81. data/lib/git/commands/branch/move.rb +94 -0
  82. data/lib/git/commands/branch/set_upstream.rb +86 -0
  83. data/lib/git/commands/branch/show_current.rb +49 -0
  84. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  85. data/lib/git/commands/branch.rb +34 -0
  86. data/lib/git/commands/cat_file/batch.rb +364 -0
  87. data/lib/git/commands/cat_file/filtered.rb +105 -0
  88. data/lib/git/commands/cat_file/raw.rb +210 -0
  89. data/lib/git/commands/cat_file.rb +49 -0
  90. data/lib/git/commands/checkout/branch.rb +151 -0
  91. data/lib/git/commands/checkout/files.rb +115 -0
  92. data/lib/git/commands/checkout.rb +38 -0
  93. data/lib/git/commands/checkout_index.rb +105 -0
  94. data/lib/git/commands/clean.rb +100 -0
  95. data/lib/git/commands/clone.rb +240 -0
  96. data/lib/git/commands/commit.rb +272 -0
  97. data/lib/git/commands/commit_tree.rb +100 -0
  98. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  99. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  100. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  101. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  102. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  103. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  104. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
  105. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  106. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  107. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  108. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  109. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  110. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  111. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  112. data/lib/git/commands/config_option_syntax.rb +56 -0
  113. data/lib/git/commands/describe.rb +155 -0
  114. data/lib/git/commands/diff.rb +656 -0
  115. data/lib/git/commands/diff_files.rb +518 -0
  116. data/lib/git/commands/diff_index.rb +496 -0
  117. data/lib/git/commands/fetch.rb +352 -0
  118. data/lib/git/commands/fsck.rb +136 -0
  119. data/lib/git/commands/gc.rb +132 -0
  120. data/lib/git/commands/grep.rb +338 -0
  121. data/lib/git/commands/init.rb +99 -0
  122. data/lib/git/commands/log.rb +632 -0
  123. data/lib/git/commands/ls_files.rb +191 -0
  124. data/lib/git/commands/ls_remote.rb +155 -0
  125. data/lib/git/commands/ls_tree.rb +131 -0
  126. data/lib/git/commands/maintenance/register.rb +75 -0
  127. data/lib/git/commands/maintenance/run.rb +104 -0
  128. data/lib/git/commands/maintenance/start.rb +66 -0
  129. data/lib/git/commands/maintenance/stop.rb +55 -0
  130. data/lib/git/commands/maintenance/unregister.rb +79 -0
  131. data/lib/git/commands/maintenance.rb +31 -0
  132. data/lib/git/commands/merge/abort.rb +44 -0
  133. data/lib/git/commands/merge/continue.rb +44 -0
  134. data/lib/git/commands/merge/quit.rb +46 -0
  135. data/lib/git/commands/merge/start.rb +245 -0
  136. data/lib/git/commands/merge.rb +28 -0
  137. data/lib/git/commands/merge_base.rb +86 -0
  138. data/lib/git/commands/mv.rb +77 -0
  139. data/lib/git/commands/name_rev.rb +114 -0
  140. data/lib/git/commands/pull.rb +377 -0
  141. data/lib/git/commands/push.rb +246 -0
  142. data/lib/git/commands/read_tree.rb +149 -0
  143. data/lib/git/commands/remote/add.rb +91 -0
  144. data/lib/git/commands/remote/get_url.rb +66 -0
  145. data/lib/git/commands/remote/list.rb +54 -0
  146. data/lib/git/commands/remote/prune.rb +61 -0
  147. data/lib/git/commands/remote/remove.rb +52 -0
  148. data/lib/git/commands/remote/rename.rb +69 -0
  149. data/lib/git/commands/remote/set_branches.rb +63 -0
  150. data/lib/git/commands/remote/set_head.rb +82 -0
  151. data/lib/git/commands/remote/set_url.rb +71 -0
  152. data/lib/git/commands/remote/set_url_add.rb +61 -0
  153. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  154. data/lib/git/commands/remote/show.rb +71 -0
  155. data/lib/git/commands/remote/update.rb +72 -0
  156. data/lib/git/commands/remote.rb +42 -0
  157. data/lib/git/commands/repack.rb +277 -0
  158. data/lib/git/commands/reset.rb +147 -0
  159. data/lib/git/commands/rev_parse.rb +297 -0
  160. data/lib/git/commands/revert/abort.rb +45 -0
  161. data/lib/git/commands/revert/continue.rb +57 -0
  162. data/lib/git/commands/revert/quit.rb +47 -0
  163. data/lib/git/commands/revert/skip.rb +44 -0
  164. data/lib/git/commands/revert/start.rb +153 -0
  165. data/lib/git/commands/revert.rb +29 -0
  166. data/lib/git/commands/rm.rb +114 -0
  167. data/lib/git/commands/show.rb +632 -0
  168. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  169. data/lib/git/commands/show_ref/exists.rb +78 -0
  170. data/lib/git/commands/show_ref/list.rb +145 -0
  171. data/lib/git/commands/show_ref/verify.rb +120 -0
  172. data/lib/git/commands/show_ref.rb +42 -0
  173. data/lib/git/commands/stash/apply.rb +75 -0
  174. data/lib/git/commands/stash/branch.rb +65 -0
  175. data/lib/git/commands/stash/clear.rb +41 -0
  176. data/lib/git/commands/stash/create.rb +58 -0
  177. data/lib/git/commands/stash/drop.rb +67 -0
  178. data/lib/git/commands/stash/list.rb +39 -0
  179. data/lib/git/commands/stash/pop.rb +78 -0
  180. data/lib/git/commands/stash/push.rb +103 -0
  181. data/lib/git/commands/stash/show.rb +149 -0
  182. data/lib/git/commands/stash/store.rb +63 -0
  183. data/lib/git/commands/stash.rb +38 -0
  184. data/lib/git/commands/status.rb +169 -0
  185. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  186. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  187. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  188. data/lib/git/commands/symbolic_ref.rb +38 -0
  189. data/lib/git/commands/tag/create.rb +139 -0
  190. data/lib/git/commands/tag/delete.rb +55 -0
  191. data/lib/git/commands/tag/list.rb +143 -0
  192. data/lib/git/commands/tag/verify.rb +71 -0
  193. data/lib/git/commands/tag.rb +26 -0
  194. data/lib/git/commands/update_ref/batch.rb +140 -0
  195. data/lib/git/commands/update_ref/delete.rb +92 -0
  196. data/lib/git/commands/update_ref/update.rb +106 -0
  197. data/lib/git/commands/update_ref.rb +42 -0
  198. data/lib/git/commands/version.rb +52 -0
  199. data/lib/git/commands/worktree/add.rb +140 -0
  200. data/lib/git/commands/worktree/list.rb +64 -0
  201. data/lib/git/commands/worktree/lock.rb +58 -0
  202. data/lib/git/commands/worktree/management_base.rb +51 -0
  203. data/lib/git/commands/worktree/move.rb +66 -0
  204. data/lib/git/commands/worktree/prune.rb +67 -0
  205. data/lib/git/commands/worktree/remove.rb +63 -0
  206. data/lib/git/commands/worktree/repair.rb +76 -0
  207. data/lib/git/commands/worktree/unlock.rb +47 -0
  208. data/lib/git/commands/worktree.rb +43 -0
  209. data/lib/git/commands/write_tree.rb +68 -0
  210. data/lib/git/commands.rb +89 -0
  211. data/lib/git/detached_head_info.rb +54 -0
  212. data/lib/git/diff.rb +297 -7
  213. data/lib/git/diff_file_numstat_info.rb +29 -0
  214. data/lib/git/diff_file_patch_info.rb +134 -0
  215. data/lib/git/diff_file_raw_info.rb +127 -0
  216. data/lib/git/diff_info.rb +169 -0
  217. data/lib/git/diff_path_status.rb +78 -19
  218. data/lib/git/diff_result.rb +32 -0
  219. data/lib/git/diff_stats.rb +59 -14
  220. data/lib/git/dirstat_info.rb +86 -0
  221. data/lib/git/errors.rb +65 -2
  222. data/lib/git/execution_context/global.rb +56 -0
  223. data/lib/git/execution_context/repository.rb +147 -0
  224. data/lib/git/execution_context.rb +482 -0
  225. data/lib/git/file_ref.rb +74 -0
  226. data/lib/git/fsck_object.rb +9 -9
  227. data/lib/git/fsck_result.rb +1 -1
  228. data/lib/git/lib.rb +1606 -1028
  229. data/lib/git/log.rb +15 -2
  230. data/lib/git/object.rb +92 -22
  231. data/lib/git/parsers/branch.rb +224 -0
  232. data/lib/git/parsers/cat_file.rb +111 -0
  233. data/lib/git/parsers/diff.rb +585 -0
  234. data/lib/git/parsers/fsck.rb +133 -0
  235. data/lib/git/parsers/grep.rb +42 -0
  236. data/lib/git/parsers/ls_tree.rb +58 -0
  237. data/lib/git/parsers/stash.rb +208 -0
  238. data/lib/git/parsers/tag.rb +257 -0
  239. data/lib/git/remote.rb +133 -9
  240. data/lib/git/repository/branching.rb +572 -0
  241. data/lib/git/repository/committing.rb +191 -0
  242. data/lib/git/repository/configuring.rb +156 -0
  243. data/lib/git/repository/diffing.rb +775 -0
  244. data/lib/git/repository/inspecting.rb +153 -0
  245. data/lib/git/repository/logging.rb +247 -0
  246. data/lib/git/repository/merging.rb +295 -0
  247. data/lib/git/repository/object_operations.rb +1101 -0
  248. data/lib/git/repository/path_resolver.rb +207 -0
  249. data/lib/git/repository/remote_operations.rb +753 -0
  250. data/lib/git/repository/shared_private.rb +51 -0
  251. data/lib/git/repository/staging.rb +390 -0
  252. data/lib/git/repository/stashing.rb +107 -0
  253. data/lib/git/repository/status_operations.rb +180 -0
  254. data/lib/git/repository/worktree_operations.rb +159 -0
  255. data/lib/git/repository.rb +264 -1
  256. data/lib/git/stash.rb +85 -4
  257. data/lib/git/stash_info.rb +104 -0
  258. data/lib/git/stashes.rb +130 -13
  259. data/lib/git/status.rb +226 -18
  260. data/lib/git/tag_delete_failure.rb +31 -0
  261. data/lib/git/tag_delete_result.rb +63 -0
  262. data/lib/git/tag_info.rb +105 -0
  263. data/lib/git/version.rb +109 -2
  264. data/lib/git/version_constraint.rb +81 -0
  265. data/lib/git/worktree.rb +120 -5
  266. data/lib/git/worktrees.rb +107 -7
  267. data/lib/git.rb +117 -56
  268. data/redesign/1_architecture_existing.md +54 -18
  269. data/redesign/2_architecture_redesign.md +365 -46
  270. data/redesign/3_architecture_implementation.md +1451 -54
  271. data/tasks/gem_tasks.rake +4 -0
  272. data/tasks/npm_tasks.rake +7 -0
  273. data/tasks/rspec.rake +48 -0
  274. data/tasks/test.rake +13 -1
  275. data/tasks/yard.rake +34 -7
  276. metadata +348 -19
  277. data/lib/git/index.rb +0 -6
  278. data/lib/git/path.rb +0 -38
  279. data/lib/git/working_directory.rb +0 -6
  280. /data/{release-please-config.json → .release-please-config.json} +0 -0
data/lib/git/status.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # These would be required by the main `git.rb` file
4
+
3
5
  module Git
4
6
  # The Status class gets the status of a git repository. It identifies which
5
7
  # files have been modified, added, or deleted, including untracked files.
@@ -10,37 +12,112 @@ module Git
10
12
  class Status
11
13
  include Enumerable
12
14
 
13
- # @param base [Git::Base] The base git object
15
+ # Create a new Status for the given repository
16
+ #
17
+ # @param base [Git::Base, Git::Repository] the git object backing this status
18
+ #
14
19
  def initialize(base)
15
20
  @base = base
16
21
  # The factory returns a hash of file paths to StatusFile objects.
17
22
  @files = StatusFileFactory.new(base).construct_files
18
23
  end
19
24
 
20
- # File status collections, memoized for performance.
25
+ # Return files modified in the index and/or working tree
26
+ #
27
+ # Includes both staged modifications (index vs HEAD) and unstaged modifications
28
+ # (working tree vs index).
29
+ #
30
+ # @return [Hash{String => Git::Status::StatusFile}] changed files keyed by path
31
+ #
21
32
  def changed = @changed ||= select_files { |f| f.type == 'M' }
33
+
34
+ # Return files added to the index that are not yet in HEAD
35
+ #
36
+ # @return [Hash{String => Git::Status::StatusFile}] added files keyed by path
37
+ #
22
38
  def added = @added ||= select_files { |f| f.type == 'A' }
39
+
40
+ # Return files deleted from the index
41
+ #
42
+ # @return [Hash{String => Git::Status::StatusFile}] deleted files keyed by path
43
+ #
23
44
  def deleted = @deleted ||= select_files { |f| f.type == 'D' }
24
- # This works with `true` or `nil`
45
+
46
+ # Return files present in the working tree but not tracked by git
47
+ #
48
+ # @return [Hash{String => Git::Status::StatusFile}] untracked files keyed by path
49
+ #
25
50
  def untracked = @untracked ||= select_files(&:untracked)
26
51
 
27
- # Predicate methods to check the status of a specific file.
52
+ # Return `true` if `file` has been modified in the index or working tree
53
+ #
54
+ # @param file [String] the repository-relative path to check
55
+ #
56
+ # @return [Boolean] `true` if the file has been modified
57
+ #
28
58
  def changed?(file) = file_in_collection?(:changed, file)
59
+
60
+ # Return `true` if `file` has been added to the index
61
+ #
62
+ # @param file [String] the repository-relative path to check
63
+ #
64
+ # @return [Boolean] `true` if the file has been added
65
+ #
29
66
  def added?(file) = file_in_collection?(:added, file)
67
+
68
+ # Return `true` if `file` has been deleted from the index
69
+ #
70
+ # @param file [String] the repository-relative path to check
71
+ #
72
+ # @return [Boolean] `true` if the file has been deleted
73
+ #
30
74
  def deleted?(file) = file_in_collection?(:deleted, file)
75
+
76
+ # Return `true` if `file` is not tracked by git
77
+ #
78
+ # @param file [String] the repository-relative path to check
79
+ #
80
+ # @return [Boolean] `true` if the file is untracked
81
+ #
31
82
  def untracked?(file) = file_in_collection?(:untracked, file)
32
83
 
33
- # Access a status file by path, or iterate over all status files.
84
+ # Return the {Git::Status::StatusFile} for the given path
85
+ #
86
+ # @param file [String] the repository-relative path
87
+ #
88
+ # @return [Git::Status::StatusFile, nil] the status file, or `nil` if not found
89
+ #
34
90
  def [](file) = @files[file]
91
+
92
+ # Iterate over all status files
93
+ #
94
+ # @yield [file] each {Git::Status::StatusFile} in the repository
95
+ #
96
+ # @yieldparam file [Git::Status::StatusFile] a single file's status
97
+ #
98
+ # @return [Enumerator<Git::Status::StatusFile>] if no block is given
99
+ #
100
+ # @return [Array<Git::Status::StatusFile>] if a block is given
101
+ #
35
102
  def each(&) = @files.values.each(&)
36
103
 
37
- # Returns a formatted string representation of the status.
104
+ # Return a formatted multi-line string representation of the status
105
+ #
106
+ # @return [String] one indented block per file showing its SHA, mode, type,
107
+ # stage, and untracked flag
108
+ #
38
109
  def pretty
39
110
  map { |file| pretty_file(file) }.join << "\n"
40
111
  end
41
112
 
42
113
  private
43
114
 
115
+ # Format a single file's status as an indented multi-line string
116
+ #
117
+ # @param file [Git::Status::StatusFile] the file to format
118
+ #
119
+ # @return [String] the formatted status block for this file
120
+ #
44
121
  def pretty_file(file)
45
122
  <<~FILE
46
123
  #{file.path}
@@ -52,10 +129,26 @@ module Git
52
129
  FILE
53
130
  end
54
131
 
132
+ # Return a hash of files for which the block returns a truthy value
133
+ #
134
+ # @yield [file] each {Git::Status::StatusFile} in the repository
135
+ #
136
+ # @yieldparam file [Git::Status::StatusFile] a single file's status
137
+ #
138
+ # @return [Hash{String => Git::Status::StatusFile}] matching files keyed by path
139
+ #
55
140
  def select_files(&block)
56
141
  @files.select { |_path, file| block.call(file) }
57
142
  end
58
143
 
144
+ # Return `true` if `file_path` exists in the named status collection
145
+ #
146
+ # @param collection_name [Symbol] the collection to check (e.g. `:changed`)
147
+ #
148
+ # @param file_path [String] the repository-relative path to look up
149
+ #
150
+ # @return [Boolean] `true` if the path is present in the collection
151
+ #
59
152
  def file_in_collection?(collection_name, file_path)
60
153
  collection = public_send(collection_name)
61
154
  if ignore_case?
@@ -65,12 +158,25 @@ module Git
65
158
  end
66
159
  end
67
160
 
161
+ # Return a memoized set of downcased keys for the named collection
162
+ #
163
+ # @param collection_name [Symbol] the collection whose keys to downcase
164
+ #
165
+ # @return [Set<String>] the lowercased path keys
166
+ #
68
167
  def downcased_keys(collection_name)
69
168
  @_downcased_keys ||= {}
70
169
  @_downcased_keys[collection_name] ||=
71
170
  public_send(collection_name).keys.to_set(&:downcase)
72
171
  end
73
172
 
173
+ # Return `true` when git is configured to ignore filename case
174
+ #
175
+ # Reads `core.ignoreCase` from the repository config. Returns `false` if
176
+ # the config value is absent or if reading it raises {Git::FailedError}.
177
+ #
178
+ # @return [Boolean] `true` when `core.ignoreCase` is `"true"`
179
+ #
74
180
  def ignore_case?
75
181
  return @_ignore_case if defined?(@_ignore_case)
76
182
 
@@ -78,13 +184,65 @@ module Git
78
184
  rescue Git::FailedError
79
185
  @_ignore_case = false
80
186
  end
187
+ end
188
+ end
81
189
 
190
+ module Git
191
+ class Status
82
192
  # Represents a single file's status in the git repository. Each instance
83
193
  # holds information about a file's state in the index and working tree.
194
+ #
195
+ # @api public
196
+ #
84
197
  class StatusFile
85
- attr_reader :path, :type, :stage, :mode_index, :mode_repo,
86
- :sha_index, :sha_repo, :untracked
198
+ # The repository-relative file path
199
+ #
200
+ # @return [String] the path
201
+ attr_reader :path
87
202
 
203
+ # The change type for this file
204
+ #
205
+ # @return [String, nil] `"M"` for modified, `"A"` for added, `"D"` for deleted,
206
+ # or `nil` when not applicable
207
+ attr_reader :type
208
+
209
+ # The merge stage for this file
210
+ #
211
+ # @return [String, nil] `"0"` for normal entries, or a non-zero value during
212
+ # a merge conflict
213
+ attr_reader :stage
214
+
215
+ # The file mode recorded in the index
216
+ #
217
+ # @return [String, nil] the octal file mode (e.g. `"100644"`), or `nil`
218
+ attr_reader :mode_index
219
+
220
+ # The file mode recorded in HEAD
221
+ #
222
+ # @return [String, nil] the octal file mode (e.g. `"100644"`), or `nil`
223
+ attr_reader :mode_repo
224
+
225
+ # The SHA of the index version of this file
226
+ #
227
+ # @return [String, nil] the SHA-1 hex digest, or `nil` if unavailable
228
+ attr_reader :sha_index
229
+
230
+ # The SHA of the HEAD version of this file
231
+ #
232
+ # @return [String, nil] the SHA-1 hex digest, or `nil` if unavailable
233
+ attr_reader :sha_repo
234
+
235
+ # Whether this file is untracked
236
+ #
237
+ # @return [Boolean, nil] `true` when the file is not tracked by git
238
+ attr_reader :untracked
239
+
240
+ # Initialize a new StatusFile with the given git object and data hash
241
+ #
242
+ # @param base [Git::Base, Git::Repository] the git object
243
+ #
244
+ # @param hash [Hash] raw status data for this file
245
+ #
88
246
  def initialize(base, hash)
89
247
  @base = base
90
248
  @path = hash[:path]
@@ -97,24 +255,51 @@ module Git
97
255
  @untracked = hash[:untracked]
98
256
  end
99
257
 
100
- # Returns a Git::Object::Blob for either the index or repo version of the file.
258
+ # Return a blob object for the index or repo version of this file
259
+ #
260
+ # @param type [Symbol] `:index` (default) for the index version, or
261
+ # `:repo` for the HEAD version
262
+ #
263
+ # @return [Git::Object::Blob, nil] the blob object, or `nil` if no SHA
264
+ # is available for the requested version
265
+ #
101
266
  def blob(type = :index)
102
267
  sha = type == :repo ? sha_repo : (sha_index || sha_repo)
103
268
  @base.object(sha) if sha
104
269
  end
105
270
  end
271
+ end
272
+ end
106
273
 
274
+ module Git
275
+ class Status
107
276
  # A factory class responsible for fetching git status data and building
108
277
  # a hash of StatusFile objects.
278
+ #
109
279
  # @api private
280
+ #
110
281
  class StatusFileFactory
282
+ # Create a new factory backed by the given git object
283
+ #
284
+ # When `base` is a {Git::Repository} (which exposes `#diff_index`
285
+ # directly), it is used as the data provider. When `base` is a
286
+ # {Git::Base} (the legacy path), `base.lib` is used instead.
287
+ #
288
+ # @param base [Git::Base, Git::Repository] the git object used as the
289
+ # status data provider
290
+ #
111
291
  def initialize(base)
112
292
  @base = base
113
- @lib = base.lib
293
+ # When base is Git::Repository (which exposes #diff_index directly),
294
+ # use it as the data provider. Otherwise use base.lib (legacy path).
295
+ @provider = base.respond_to?(:diff_index) ? base : base.lib
114
296
  end
115
297
 
116
- # Gathers all status data and builds a hash of file paths to
117
- # StatusFile objects.
298
+ # Gather all status data and build a hash of file paths to StatusFile objects
299
+ #
300
+ # @return [Hash{String => Git::Status::StatusFile}] file paths mapped to
301
+ # their status objects
302
+ #
118
303
  def construct_files
119
304
  files_data = fetch_all_files_data
120
305
  files_data.transform_values do |data|
@@ -124,33 +309,56 @@ module Git
124
309
 
125
310
  private
126
311
 
127
- # Fetches and merges status information from multiple git commands.
312
+ # Fetch and merge status information from multiple git commands
313
+ #
314
+ # @return [Hash{String => Hash}] raw per-file status data keyed by path
315
+ #
128
316
  def fetch_all_files_data
129
- files = @lib.ls_files # Start with files tracked in the index.
317
+ files = @provider.ls_files # Start with files tracked in the index.
130
318
  merge_untracked_files(files)
131
319
  merge_modified_files(files)
132
320
  merge_head_diffs(files)
133
321
  files
134
322
  end
135
323
 
324
+ # Merge untracked working-tree files into `files`
325
+ #
326
+ # @param files [Hash] the in-progress files hash to update in place
327
+ #
328
+ # @return [void]
329
+ #
136
330
  def merge_untracked_files(files)
137
- @lib.untracked_files.each do |file|
331
+ @provider.untracked_files.each do |file|
138
332
  files[file] = { path: file, untracked: true }
139
333
  end
140
334
  end
141
335
 
336
+ # Merge index-versus-working-tree diff data into `files`
337
+ #
338
+ # @param files [Hash] the in-progress files hash to update in place
339
+ #
340
+ # @return [void]
341
+ #
142
342
  def merge_modified_files(files)
143
343
  # Merge changes between the index and the working directory.
144
- @lib.diff_files.each do |path, data|
344
+ @provider.diff_files.each do |path, data|
145
345
  (files[path] ||= {}).merge!(data)
146
346
  end
147
347
  end
148
348
 
349
+ # Merge HEAD-versus-index diff data into `files`, if commits exist
350
+ #
351
+ # @param files [Hash] the in-progress files hash to update in place
352
+ #
353
+ # @return [void]
354
+ #
149
355
  def merge_head_diffs(files)
150
- return if @lib.empty?
356
+ # Git::Repository exposes #no_commits?; Git::Lib exposes #empty?.
357
+ is_empty = @provider.respond_to?(:no_commits?) ? @provider.no_commits? : @provider.empty?
358
+ return if is_empty
151
359
 
152
360
  # Merge changes between HEAD and the index.
153
- @lib.diff_index('HEAD').each do |path, data|
361
+ @provider.diff_index('HEAD').each do |path, data|
154
362
  (files[path] ||= {}).merge!(data)
155
363
  end
156
364
  end
@@ -0,0 +1,31 @@
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
+ # @see Git::Commands::Tag::Delete
19
+ #
20
+ # @api public
21
+ #
22
+ # @!attribute [r] name
23
+ # The name of the tag that failed to be deleted
24
+ # @return [String]
25
+ #
26
+ # @!attribute [r] error_message
27
+ # The error message from git explaining why the tag could not be deleted
28
+ # @return [String]
29
+ #
30
+ TagDeleteFailure = Data.define(:name, :error_message)
31
+ end
@@ -0,0 +1,63 @@
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
+ # @see Git::TagDeleteFailure
33
+ # @see Git::Commands::Tag::Delete
34
+ #
35
+ # @api public
36
+ #
37
+ # @!attribute [r] deleted
38
+ # Tags that were successfully deleted
39
+ # @return [Array<Git::TagInfo>]
40
+ #
41
+ # @!attribute [r] not_deleted
42
+ # Tags that could not be deleted, with the reason for each failure
43
+ # @return [Array<Git::TagDeleteFailure>]
44
+ #
45
+ TagDeleteResult = Data.define(:deleted, :not_deleted) do
46
+ # Returns true if all requested tags were successfully deleted
47
+ #
48
+ # @return [Boolean] true if no tags failed to delete, false otherwise
49
+ #
50
+ # @example
51
+ # result = tag_delete.call('v1.0.0')
52
+ # if result.success?
53
+ # puts "All tags deleted successfully"
54
+ # else
55
+ # puts "Some tags could not be deleted:"
56
+ # result.not_deleted.each { |f| puts " #{f.name}: #{f.error_message}" }
57
+ # end
58
+ #
59
+ def success?
60
+ not_deleted.empty?
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/author'
4
+
5
+ module Git
6
+ # Value object representing tag metadata from git tag output
7
+ #
8
+ # This is a lightweight, immutable data structure returned by tag listing
9
+ # commands. It contains only the data parsed from git output without any
10
+ # repository context or operations.
11
+ #
12
+ # @example Annotated tag
13
+ # info = Git::TagInfo.new(
14
+ # name: 'v1.0.0',
15
+ # oid: 'abc123def456', # tag object's ID
16
+ # target_oid: 'def456abc789', # commit it points to
17
+ # objecttype: 'tag',
18
+ # tagger_name: 'John Doe',
19
+ # tagger_email: '<john@example.com>',
20
+ # tagger_date: '2024-01-15T10:30:00-08:00',
21
+ # message: 'Release version 1.0.0'
22
+ # )
23
+ # info.annotated? #=> true
24
+ # info.tagger.name #=> 'John Doe'
25
+ #
26
+ # @example Lightweight tag
27
+ # info = Git::TagInfo.new(
28
+ # name: 'v1.0.0',
29
+ # oid: nil, # no tag object exists
30
+ # target_oid: 'def456abc789', # commit ID
31
+ # objecttype: 'commit',
32
+ # tagger_name: nil,
33
+ # tagger_email: nil,
34
+ # tagger_date: nil,
35
+ # message: nil
36
+ # )
37
+ # info.lightweight? #=> true
38
+ # info.tagger #=> nil
39
+ #
40
+ # @see Git::Tag for the full-featured tag object with operations
41
+ # @see Git::Commands::Tag::List for the command that produces these
42
+ #
43
+ # @api public
44
+ #
45
+ # @!attribute [r] name
46
+ # @return [String] the tag name (e.g., 'v1.0.0')
47
+ #
48
+ # @!attribute [r] oid
49
+ # The object ID of the tag object itself.
50
+ #
51
+ # For annotated tags, this is the tag object's ID. For lightweight tags,
52
+ # this is nil because lightweight tags are not objects in the git database.
53
+ #
54
+ # @return [String, nil] the tag object's ID, or nil for lightweight tags
55
+ #
56
+ # @!attribute [r] target_oid
57
+ # The object ID of the commit this tag ultimately points to.
58
+ #
59
+ # For both annotated and lightweight tags, this is the commit ID that the
60
+ # tag resolves to (i.e., the dereferenced target).
61
+ #
62
+ # @return [String] the commit ID this tag points to
63
+ #
64
+ # @!attribute [r] objecttype
65
+ # @return [String] 'tag' for annotated tags, 'commit' for lightweight tags
66
+ #
67
+ # @!attribute [r] tagger_name
68
+ # @return [String, nil] the tagger's name, or nil for lightweight tags
69
+ #
70
+ # @!attribute [r] tagger_email
71
+ # @return [String, nil] the tagger's email, or nil for lightweight tags
72
+ #
73
+ # @!attribute [r] tagger_date
74
+ # @return [String, nil] the tag date in ISO 8601 format, or nil for lightweight tags
75
+ #
76
+ # @!attribute [r] message
77
+ # @return [String, nil] the tag message, or nil for lightweight tags
78
+ #
79
+ TagInfo = Data.define(:name, :oid, :target_oid, :objecttype, :tagger_name, :tagger_email, :tagger_date, :message) do
80
+ # @return [Boolean] true if this is an annotated tag (oid is present)
81
+ def annotated?
82
+ !oid.nil?
83
+ end
84
+
85
+ # @return [Boolean] true if this is a lightweight tag (oid is nil)
86
+ def lightweight?
87
+ oid.nil?
88
+ end
89
+
90
+ # Return the tagger as an Author object
91
+ #
92
+ # @return [Git::Author, nil] the tagger as an Author object, or nil for lightweight tags
93
+ def tagger
94
+ return nil unless annotated? && tagger_name && tagger_email
95
+
96
+ # Git::Author expects format "Name <email> timestamp timezone"
97
+ # We construct a minimal format that will parse correctly
98
+ author = Git::Author.new('')
99
+ author.name = tagger_name
100
+ # Remove angle brackets if present
101
+ author.email = tagger_email.gsub(/\A<|>\z/, '')
102
+ author
103
+ end
104
+ end
105
+ end
data/lib/git/version.rb CHANGED
@@ -2,6 +2,113 @@
2
2
 
3
3
  module Git
4
4
  # The current gem version
5
- # @return [String] the current gem version.
6
- VERSION = '4.4.0'
5
+ #
6
+ # @return [String] the current gem version
7
+ VERSION = '5.0.0.beta.1'
8
+
9
+ # Represents a git version with major, minor, and patch components
10
+ #
11
+ # Git versions follow a strict major.minor.patch format. This class provides
12
+ # parsing from git command output (which may include platform suffixes) and
13
+ # comparison operations for version gating.
14
+ #
15
+ # @!attribute [r] major
16
+ # The major version number
17
+ # @return [Integer]
18
+ #
19
+ # @!attribute [r] minor
20
+ # The minor version number
21
+ # @return [Integer]
22
+ #
23
+ # @!attribute [r] patch
24
+ # The patch version number
25
+ # @return [Integer]
26
+ #
27
+ # @example Creating a version directly
28
+ # version = Git::Version.new(2, 42, 1)
29
+ # version.to_s #=> "2.42.1"
30
+ #
31
+ # @example Parsing from git version output
32
+ # Git::Version.parse('git version 2.42.1') #=> Git::Version.new(2, 42, 1)
33
+ # Git::Version.parse('2.39.2 (Apple Git-143)') #=> Git::Version.new(2, 39, 2)
34
+ #
35
+ # @example Parsing versions with platform suffixes
36
+ # Git::Version.parse('2.42.0.windows.1') #=> Git::Version.new(2, 42, 0)
37
+ #
38
+ # @example Comparing versions
39
+ # Git::Version.new(2, 42, 1) > Git::Version.new(2, 28, 0) #=> true
40
+ #
41
+ # @api public
42
+ #
43
+ Version = Data.define(:major, :minor, :patch) do
44
+ include Comparable
45
+
46
+ # Parse a version string into a Version object
47
+ #
48
+ # Handles git's version output format, stripping platform suffixes
49
+ # (like `.windows.1` or `.vfs.0`) and padding two-segment versions
50
+ # to three segments.
51
+ #
52
+ # @example Parse various version string formats
53
+ # Git::Version.parse('2.42.1') #=> Git::Version.new(2, 42, 1)
54
+ # Git::Version.parse('git version 2.42.1') #=> Git::Version.new(2, 42, 1)
55
+ # Git::Version.parse('2.42.0.windows.1') #=> Git::Version.new(2, 42, 0)
56
+ #
57
+ # @param string [String] version string to parse
58
+ #
59
+ # @return [Git::Version] the parsed version
60
+ #
61
+ # @raise [Git::UnexpectedResultError] if the string cannot be parsed as a version
62
+ #
63
+ def self.parse(string)
64
+ version_match = string&.match(/(\d+)\.(\d+)(?:\.(\d+))?/)
65
+ raise Git::UnexpectedResultError, "Invalid version: #{string.inspect}" unless version_match
66
+
67
+ major = version_match[1].to_i
68
+ minor = version_match[2].to_i
69
+ patch = (version_match[3] || '0').to_i
70
+
71
+ new(major, minor, patch)
72
+ end
73
+
74
+ # Compare this version to another
75
+ #
76
+ # @param other [Git::Version] the version to compare to
77
+ #
78
+ # @return [Integer] -1, 0, or 1
79
+ #
80
+ def <=>(other)
81
+ [major, minor, patch] <=> [other.major, other.minor, other.patch]
82
+ end
83
+
84
+ # Return the version as a dotted string
85
+ #
86
+ # @return [String] the version in "major.minor.patch" format
87
+ #
88
+ def to_s
89
+ "#{major}.#{minor}.#{patch}"
90
+ end
91
+
92
+ # Return a readable representation
93
+ #
94
+ # @return [String] inspect string
95
+ #
96
+ def inspect
97
+ "#<Git::Version #{self}>"
98
+ end
99
+
100
+ # Return the version as an array of integers
101
+ #
102
+ # Useful when legacy code expects the array shape returned by the
103
+ # deprecated {Git::Lib#current_command_version} method.
104
+ #
105
+ # @return [Array<Integer>] [major, minor, patch]
106
+ #
107
+ # @example
108
+ # Git.git_version.to_a #=> [2, 42, 0]
109
+ #
110
+ def to_a
111
+ deconstruct
112
+ end
113
+ end
7
114
  end