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,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/ls_files'
4
+ require 'git/commands/rev_parse'
5
+ require 'git/commands/status'
6
+ require 'git/escaped_path'
7
+ require 'git/parsers/status'
8
+ require 'git/status'
9
+ require 'git/status_info'
10
+
11
+ module Git
12
+ class Repository
13
+ # Facade methods for repository-status operations
14
+ #
15
+ # Provides methods for querying the state of the repository: checking
16
+ # whether any commits exist, listing untracked working-tree files, and
17
+ # listing files tracked in the index.
18
+ #
19
+ # Included by {Git::Repository}.
20
+ #
21
+ # @api private
22
+ #
23
+ module StatusOperations
24
+ # Returns `true` if the repository has no commits yet
25
+ #
26
+ # Checks whether `HEAD` can be resolved to a commit object. A brand-new
27
+ # repository (or one created with `git checkout --orphan`) where no commit
28
+ # has been made yet will have no commits.
29
+ #
30
+ # @example Check whether a repository is empty
31
+ # repo.no_commits? #=> true # freshly initialized, no commits yet
32
+ # repo.no_commits? #=> false # at least one commit exists
33
+ #
34
+ # @return [Boolean] `true` when the repository has no commits, `false` otherwise
35
+ #
36
+ # @raise [Git::FailedError] if git exits with a non-zero exit status other
37
+ # than when the repository has no commits
38
+ #
39
+ def no_commits?
40
+ Git::Commands::RevParse.new(@execution_context).call('HEAD', verify: true)
41
+ false
42
+ rescue Git::FailedError => e
43
+ raise unless e.result.status.exitstatus == 128 &&
44
+ e.result.stderr == 'fatal: Needed a single revision'
45
+
46
+ true
47
+ end
48
+
49
+ # Returns `true` if the repository has no commits yet
50
+ #
51
+ # @example Check whether a repository is empty
52
+ # repo.empty? #=> true # freshly initialized, no commits yet
53
+ # repo.empty? #=> false # at least one commit exists
54
+ #
55
+ # @return [Boolean] `true` when the repository has no commits, `false` otherwise
56
+ #
57
+ # @raise [Git::FailedError] if git exits with a non-zero exit status other
58
+ # than when the repository has no commits
59
+ #
60
+ # @deprecated Use {#no_commits?} instead
61
+ #
62
+ def empty?
63
+ Git::Deprecation.warn(
64
+ 'Git::Repository#empty? is deprecated and will be removed in v6.0.0. ' \
65
+ 'Use Git::Repository#no_commits? instead.'
66
+ )
67
+ no_commits?
68
+ end
69
+
70
+ # List all files in the working tree that are not tracked by git
71
+ #
72
+ # Runs `git ls-files --others --exclude-standard` from the working tree
73
+ # root and returns an array of repository-relative file paths. Files that
74
+ # match `.gitignore` or other standard exclusion rules are omitted.
75
+ #
76
+ # @example Get untracked files
77
+ # repo.untracked_files #=> ["new_feature.rb", "tmp/debug.log"]
78
+ #
79
+ # @example No untracked files
80
+ # repo.untracked_files #=> []
81
+ #
82
+ # @return [Array<String>] repository-relative paths of untracked,
83
+ # non-ignored files; empty when there are none
84
+ #
85
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
86
+ #
87
+ def untracked_files
88
+ Git::Commands::LsFiles.new(@execution_context).call(
89
+ others: true, exclude_standard: true, chdir: @execution_context.git_work_dir
90
+ ).stdout.split("\n").map { |f| Private.unescape_quoted_path(f) }
91
+ end
92
+
93
+ # Returns a {Git::StatusInfo} describing the index and working tree state
94
+ #
95
+ # Runs `git status` in porcelain v2 format with NUL-separated entries and
96
+ # every untracked file listed individually, then reads `core.ignoreCase`
97
+ # as a boolean so that the path predicates on the result compare paths the
98
+ # way git does in this repository. Every entry type git reports is
99
+ # represented, including renames, copies, and merge conflicts. Clean
100
+ # tracked paths are not reported by `git status`, so they are absent from
101
+ # the result; the deprecated {#status} listed them, and {#ls_files} still
102
+ # does.
103
+ #
104
+ # @example Check which files are modified
105
+ # repo.status_info.changed
106
+ # #=> { "lib/foo.rb" => #<data Git::StatusFileInfo path="lib/foo.rb", ...> }
107
+ #
108
+ # @example Check for untracked files
109
+ # repo.status_info.untracked.keys #=> ["new_file.rb"]
110
+ #
111
+ # @example Check one path
112
+ # repo.status_info.changed?('lib/foo.rb') #=> true
113
+ #
114
+ # @example Iterate over every entry
115
+ # repo.status_info.files.each do |file|
116
+ # puts "#{file.index_status}#{file.worktree_status} #{file.path}"
117
+ # end
118
+ #
119
+ # @return [Git::StatusInfo] the status of the repository
120
+ #
121
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
122
+ #
123
+ # @see https://git-scm.com/docs/git-status git-status
124
+ #
125
+ def status_info
126
+ result = Git::Commands::Status.new(@execution_context).call(
127
+ porcelain: 'v2', z: true, untracked_files: 'all'
128
+ )
129
+ files = Git::Parsers::Status.parse(result.stdout)
130
+ ignore_case = config_get('core.ignoreCase', type: 'bool')&.value == 'true'
131
+ Git::StatusInfo.new(files: files, ignore_case: ignore_case)
132
+ end
133
+
134
+ # Returns a {Git::Status} object describing the working tree and index state
135
+ #
136
+ # Constructs a {Git::Status} for this repository by collecting information from
137
+ # `git ls-files --stage`, `git ls-files --others`, `git diff-files`, and
138
+ # `git diff-index HEAD` (the last only when at least one commit exists). The
139
+ # result identifies which files have been modified, added, deleted, or are
140
+ # untracked.
141
+ #
142
+ # Emits one deprecation warning per call. The {Git::Status} it constructs is
143
+ # built with warnings silenced so the caller does not see a second one.
144
+ #
145
+ # @example Check which files are modified (deprecated; use status_info)
146
+ # repo.status.changed.keys #=> ["lib/foo.rb"]
147
+ # repo.status_info.changed.keys #=> ["lib/foo.rb"]
148
+ #
149
+ # @return [Git::Status] the status of the repository
150
+ #
151
+ # @raise [Git::FailedError] if any underlying git command exits with a
152
+ # non-zero exit status
153
+ #
154
+ # @deprecated Use {#status_info} instead
155
+ #
156
+ def status
157
+ Git::Deprecation.warn(
158
+ 'Git::Repository#status is deprecated and will be removed in v6.0.0. ' \
159
+ 'Use Git::Repository#status_info instead.'
160
+ )
161
+ Git::Deprecation.silence { Git::Status.new(self) }
162
+ end
163
+
164
+ # List all files tracked in the index
165
+ #
166
+ # Runs `git ls-files --stage` under the given `location` and returns a
167
+ # hash keyed by file path with per-file index metadata.
168
+ #
169
+ # @example List all indexed files in the working tree
170
+ # repo.ls_files
171
+ # #=> { "README.md" => { path: "README.md", mode_index: "100644",
172
+ # #=> sha_index: "abc123...", stage: "0" }, ... }
173
+ #
174
+ # @example List indexed files under a specific directory
175
+ # repo.ls_files('lib/')
176
+ # #=> { "lib/git.rb" => { path: "lib/git.rb", ... }, ... }
177
+ #
178
+ # @param location [String, nil] the path to restrict the listing to;
179
+ # defaults to `'.'` (all tracked files) when `nil`
180
+ #
181
+ # @return [Hash{String => Hash}] a hash of index entries keyed by file path
182
+ #
183
+ # Each value is a Hash with the following keys:
184
+ # * `:path` [String] the file path
185
+ # * `:mode_index` [String] the file's index mode (e.g. `"100644"`)
186
+ # * `:sha_index` [String] the file's index SHA
187
+ # * `:stage` [String] the merge stage (`"0"` for normal entries)
188
+ #
189
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
190
+ #
191
+ def ls_files(location = nil)
192
+ location ||= '.'
193
+ {}.tap do |files|
194
+ Git::Commands::LsFiles.new(@execution_context).call(location, stage: true).stdout.split("\n").each do |line|
195
+ info, file = Private.split_status_line(line)
196
+ mode, sha, stage = info.split
197
+ files[file] = { path: file, mode_index: mode, sha_index: sha, stage: stage }
198
+ end
199
+ end
200
+ end
201
+
202
+ # Private helpers local to {Git::Repository::StatusOperations}
203
+ #
204
+ # @api private
205
+ #
206
+ module Private
207
+ module_function
208
+
209
+ # Split a tab-delimited status line from `git ls-files --stage` output
210
+ #
211
+ # The output format is `<mode> <sha> <stage>\t<file>`. Quoted file paths
212
+ # (which git uses when the path contains non-ASCII or special characters)
213
+ # are unescaped before being returned. `line` is assumed to be non-empty
214
+ # because `git ls-files --stage` never emits blank lines.
215
+ #
216
+ # @param line [String] a single line of git ls-files output
217
+ #
218
+ # @return [Array<String>] the tab-delimited parts with the last part
219
+ # unescaped when it was git-quoted
220
+ #
221
+ def split_status_line(line)
222
+ parts = line.split("\t")
223
+ parts[-1] = unescape_quoted_path(parts[-1])
224
+ parts
225
+ end
226
+
227
+ # Unescape a git-quoted path
228
+ #
229
+ # Git wraps paths containing non-ASCII or special characters in
230
+ # double-quotes and octal-escapes each byte. This method strips the
231
+ # surrounding quotes and delegates unescaping to {Git::EscapedPath}.
232
+ #
233
+ # @param path [String] the path as it appears in git output
234
+ #
235
+ # @return [String] the unescaped path
236
+ #
237
+ def unescape_quoted_path(path)
238
+ if path.start_with?('"') && path.end_with?('"')
239
+ Git::EscapedPath.new(path[1..-2]).unescape
240
+ else
241
+ path
242
+ end
243
+ end
244
+ end
245
+
246
+ private_constant :Private
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/worktree'
4
+ require 'git/parsers/worktree'
5
+ require 'git/worktree'
6
+ require 'git/worktree_info'
7
+ require 'git/worktrees'
8
+
9
+ module Git
10
+ class Repository
11
+ # Facade methods for worktree operations: listing, adding, removing, moving,
12
+ # locking, repairing, and pruning worktrees
13
+ #
14
+ # Included by {Git::Repository}.
15
+ #
16
+ # @api private
17
+ #
18
+ module WorktreeOperations
19
+ # Returns every worktree attached to the repository
20
+ #
21
+ # Lists the main worktree first, then each linked worktree, in the order
22
+ # git reports them. The main worktree of a bare repository is included
23
+ # with {Git::WorktreeInfo#bare?} true and no head or branch.
24
+ #
25
+ # @example List all worktrees
26
+ # repo.worktree_list.map(&:path)
27
+ # #=> ["/path/to/main", "/tmp/feature"]
28
+ #
29
+ # @example Find the worktree that has a branch checked out
30
+ # info = repo.worktree_list.find { |w| w.branch == 'refs/heads/feature' }
31
+ # info.path #=> "/tmp/feature"
32
+ # info.head #=> "b8c63202c3c0ebd37b7e45fd0c22e6c20d5bead1"
33
+ # info.locked? #=> false
34
+ #
35
+ # @return [Array<Git::WorktreeInfo>] one entry per worktree
36
+ #
37
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
38
+ #
39
+ # @raise [Git::UnexpectedResultError] if the worktree listing cannot be
40
+ # parsed
41
+ #
42
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
43
+ #
44
+ def worktree_list
45
+ result = Git::Commands::Worktree::List.new(@execution_context).call(porcelain: true)
46
+ Git::Parsers::Worktree.parse_list(result.stdout)
47
+ end
48
+
49
+ # Returns all worktrees as an array of directory and SHA pairs
50
+ #
51
+ # Lists the main worktree and all linked worktrees. The main worktree of
52
+ # a bare repository has no checked-out commit and is omitted.
53
+ #
54
+ # @example List all worktrees
55
+ # repo.worktrees_all
56
+ # #=> [["/path/to/main", "4bef5ab..."], ["/tmp/worktree-1", "b8c6320..."]]
57
+ #
58
+ # @return [Array<Array(String, String)>] array of `[directory, sha]` pairs
59
+ #
60
+ # `directory` is the worktree path reported by git (absolute or relative,
61
+ # depending on repository configuration); `sha` is the full SHA of the
62
+ # checked-out HEAD commit
63
+ #
64
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
65
+ #
66
+ # @deprecated Use {#worktree_list} instead
67
+ #
68
+ # {#worktree_list} returns one {Git::WorktreeInfo} per worktree, with
69
+ # `path` and `head` in place of the pair, and includes the main worktree
70
+ # of a bare repository.
71
+ #
72
+ # @see #worktree_list
73
+ #
74
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
75
+ #
76
+ def worktrees_all
77
+ Git::Deprecation.warn(
78
+ 'Git::Repository#worktrees_all is deprecated and will be removed in v6.0.0. ' \
79
+ 'Use Git::Repository#worktree_list instead.'
80
+ )
81
+ worktree_list.reject { |worktree| worktree.head.nil? }.map { |worktree| [worktree.path, worktree.head] }
82
+ end
83
+
84
+ # Create a new linked worktree at the given directory
85
+ #
86
+ # @example Create a worktree at a path (auto-creates a branch)
87
+ # repo.worktree_add('/tmp/feature')
88
+ #
89
+ # @example Create a worktree and check out an existing commitish
90
+ # repo.worktree_add('/tmp/hotfix', 'main')
91
+ #
92
+ # @param dir [String] filesystem path for the new worktree
93
+ #
94
+ # @param commitish [String, nil] branch, tag, or commit to check out
95
+ #
96
+ # When `nil`, git creates a new branch named after the final path component
97
+ #
98
+ # @return [String] the output from the git worktree add command
99
+ #
100
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
101
+ #
102
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
103
+ #
104
+ def worktree_add(dir, commitish = nil)
105
+ args = [dir]
106
+ args << commitish unless commitish.nil?
107
+
108
+ Git::Commands::Worktree::Add.new(@execution_context).call(*args).stdout
109
+ end
110
+
111
+ # Remove a linked worktree
112
+ #
113
+ # @example Remove a worktree by path
114
+ # repo.worktree_remove('/tmp/feature')
115
+ #
116
+ # @example Remove a worktree from the list
117
+ # info = repo.worktree_list.find { |w| w.branch == 'refs/heads/feature' }
118
+ # repo.worktree_remove(info)
119
+ #
120
+ # @param worktree [String, Git::WorktreeInfo] the path of the worktree to
121
+ # remove, or its entry from {#worktree_list}
122
+ #
123
+ # @return [String] the output from the git worktree remove command
124
+ # (typically empty)
125
+ #
126
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
127
+ #
128
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
129
+ #
130
+ def worktree_remove(worktree)
131
+ Git::Commands::Worktree::Remove.new(@execution_context).call(worktree.to_s).stdout
132
+ end
133
+
134
+ # Move a linked worktree to a new location
135
+ #
136
+ # @example Move a worktree
137
+ # repo.worktree_move('/tmp/feature', '/tmp/feature-moved')
138
+ #
139
+ # @example Move a locked worktree
140
+ # repo.worktree_move('/tmp/feature', '/tmp/feature-moved', force: 2)
141
+ #
142
+ # @param worktree [String, Git::WorktreeInfo] the path of the worktree to
143
+ # move, or its entry from {#worktree_list}
144
+ #
145
+ # @param new_path [String] the destination path
146
+ #
147
+ # @param opts [Hash] options for the move
148
+ #
149
+ # @option opts [Boolean, Integer, nil] :force (nil) override git's
150
+ # safeguards; git refuses to move a locked worktree unless the flag is
151
+ # given twice, so pass `2` for that
152
+ #
153
+ # @return [String] the output from the git worktree move command
154
+ # (typically empty)
155
+ #
156
+ # @raise [ArgumentError] if unsupported options are provided
157
+ #
158
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
159
+ #
160
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
161
+ #
162
+ def worktree_move(worktree, new_path, opts = {})
163
+ Git::Commands::Worktree::Move.new(@execution_context).call(worktree.to_s, new_path, **opts).stdout
164
+ end
165
+
166
+ # Lock a linked worktree so that `git worktree prune` leaves it alone
167
+ #
168
+ # Lock a worktree whose directory is on removable media or a network
169
+ # share that is not always mounted.
170
+ #
171
+ # @example Lock a worktree
172
+ # repo.worktree_lock('/tmp/feature')
173
+ #
174
+ # @example Lock a worktree with a reason
175
+ # repo.worktree_lock('/tmp/feature', reason: 'on an external drive')
176
+ #
177
+ # @param worktree [String, Git::WorktreeInfo] the path of the worktree to
178
+ # lock, or its entry from {#worktree_list}
179
+ #
180
+ # @param opts [Hash] options for the lock
181
+ #
182
+ # @option opts [String, nil] :reason (nil) an explanation stored with the
183
+ # lock and reported as {Git::WorktreeInfo#lock_reason}
184
+ #
185
+ # @return [String] the output from the git worktree lock command
186
+ # (typically empty)
187
+ #
188
+ # @raise [ArgumentError] if unsupported options are provided
189
+ #
190
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
191
+ #
192
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
193
+ #
194
+ def worktree_lock(worktree, opts = {})
195
+ Git::Commands::Worktree::Lock.new(@execution_context).call(worktree.to_s, **opts).stdout
196
+ end
197
+
198
+ # Unlock a linked worktree
199
+ #
200
+ # @example Unlock a worktree
201
+ # repo.worktree_unlock('/tmp/feature')
202
+ #
203
+ # @param worktree [String, Git::WorktreeInfo] the path of the worktree to
204
+ # unlock, or its entry from {#worktree_list}
205
+ #
206
+ # @return [String] the output from the git worktree unlock command
207
+ # (typically empty)
208
+ #
209
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
210
+ #
211
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
212
+ #
213
+ def worktree_unlock(worktree)
214
+ Git::Commands::Worktree::Unlock.new(@execution_context).call(worktree.to_s).stdout
215
+ end
216
+
217
+ # Repair the links between the repository and its linked worktrees
218
+ #
219
+ # With no paths, repairs the link from each linked worktree back to the
220
+ # repository, which is needed after the repository directory was moved.
221
+ # Given the current paths of linked worktrees that were moved without
222
+ # {#worktree_move}, also repairs the repository's links to them.
223
+ #
224
+ # @example Repair after the repository directory was moved
225
+ # repo.worktree_repair
226
+ #
227
+ # @example Repair after a linked worktree was moved by hand
228
+ # repo.worktree_repair('/new/path/to/feature')
229
+ #
230
+ # @param paths [Array<String, Git::WorktreeInfo>] the current paths of the
231
+ # worktrees to repair, or their entries from {#worktree_list}
232
+ #
233
+ # @return [String] the output from the git worktree repair command, which
234
+ # reports each repair made
235
+ #
236
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
237
+ #
238
+ # @raise [Git::VersionError] if the installed git is older than 2.29.0
239
+ #
240
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
241
+ #
242
+ def worktree_repair(*paths)
243
+ Git::Commands::Worktree::Repair.new(@execution_context).call(*paths.map(&:to_s)).stdout
244
+ end
245
+
246
+ # Prune stale worktree administrative files
247
+ #
248
+ # Removes stale administrative files from `$GIT_DIR/worktrees`. A
249
+ # worktree becomes stale when its directory no longer exists on disk.
250
+ #
251
+ # @example Prune stale worktrees
252
+ # repo.worktree_prune
253
+ #
254
+ # @return [String] the output from the git worktree prune command
255
+ # (typically empty)
256
+ #
257
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
258
+ #
259
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
260
+ #
261
+ def worktree_prune
262
+ Git::Commands::Worktree::Prune.new(@execution_context).call.stdout
263
+ end
264
+
265
+ # Return a {Git::Worktree} object for the given directory and optional commitish
266
+ #
267
+ # This is a factory method — it constructs the domain object but does not
268
+ # immediately execute any git commands.
269
+ #
270
+ # @example Get a worktree object for a new path
271
+ # wt = repo.worktree('/tmp/feature')
272
+ #
273
+ # @example Get a worktree object for a specific branch or commit
274
+ # wt = repo.worktree('/tmp/hotfix', 'main')
275
+ #
276
+ # @param dir [String] filesystem path for the worktree
277
+ #
278
+ # @param commitish [String, nil] branch, tag, or commit to associate with
279
+ # the worktree; `nil` means no commitish is specified
280
+ #
281
+ # @return [Git::Worktree] a worktree domain object for the given path
282
+ #
283
+ # @deprecated Use {#worktree_add} and {#worktree_remove} instead
284
+ #
285
+ # `repo.worktree(dir, commitish).add` becomes
286
+ # `repo.worktree_add(dir, commitish)` and `repo.worktree(dir).remove`
287
+ # becomes `repo.worktree_remove(dir)`. Read a worktree's checked-out
288
+ # commit from {Git::WorktreeInfo#head} via {#worktree_list}.
289
+ #
290
+ # @see #worktree_add
291
+ #
292
+ # @see #worktree_remove
293
+ #
294
+ def worktree(dir, commitish = nil)
295
+ Git::Deprecation.warn(
296
+ 'Git::Repository#worktree is deprecated and will be removed in v6.0.0. ' \
297
+ 'Use Git::Repository#worktree_add and Git::Repository#worktree_remove instead.'
298
+ )
299
+ Git::Worktree.new(self, dir, commitish)
300
+ end
301
+
302
+ # Return a {Git::Worktrees} collection of all worktrees (main and linked)
303
+ #
304
+ # The collection is populated eagerly when this method is called (git runs
305
+ # at construction time). It is enumerable and supports indexed access by
306
+ # worktree path.
307
+ #
308
+ # @example Iterate over all worktrees
309
+ # repo.worktrees.each { |wt| puts wt.dir }
310
+ #
311
+ # @example Count worktrees
312
+ # repo.worktrees.size
313
+ #
314
+ # @example Access a specific worktree by path
315
+ # repo.worktrees['/tmp/feature']
316
+ #
317
+ # @return [Git::Worktrees] an enumerable collection of all worktrees
318
+ #
319
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
320
+ #
321
+ # @deprecated Use {#worktree_list} instead
322
+ #
323
+ # {#worktree_list} returns `Array<Git::WorktreeInfo>`. Look a worktree up
324
+ # by path with `worktree_list.find { |w| w.path == path }` in place of
325
+ # `worktrees[path]`, and call {#worktree_prune} in place of
326
+ # `worktrees.prune`. Calling this method emits one deprecation warning.
327
+ #
328
+ # @see #worktree_list
329
+ #
330
+ def worktrees
331
+ Git::Deprecation.warn(
332
+ 'Git::Repository#worktrees is deprecated and will be removed in v6.0.0. ' \
333
+ 'Use Git::Repository#worktree_list instead.'
334
+ )
335
+ Git::Worktrees.new(self)
336
+ end
337
+ end
338
+ end
339
+ end