git 4.3.2 → 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 -40
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -5
  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 +486 -410
  52. data/lib/git/branch.rb +380 -43
  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 +224 -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 +114 -18
  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 +349 -20
  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
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/ls_files'
4
+ require 'git/commands/rev_parse'
5
+ require 'git/escaped_path'
6
+ require 'git/status'
7
+
8
+ module Git
9
+ class Repository
10
+ # Facade methods for repository-status operations
11
+ #
12
+ # Provides methods for querying the state of the repository: checking
13
+ # whether any commits exist, listing untracked working-tree files, and
14
+ # listing files tracked in the index.
15
+ #
16
+ # Included by {Git::Repository}.
17
+ #
18
+ # @api public
19
+ #
20
+ module StatusOperations
21
+ # Returns `true` if the repository has no commits yet
22
+ #
23
+ # Checks whether `HEAD` can be resolved to a commit object. A brand-new
24
+ # repository (or one created with `git checkout --orphan`) where no commit
25
+ # has been made yet will have no commits.
26
+ #
27
+ # @example Check whether a repository is empty
28
+ # repo.no_commits? #=> true # freshly initialized, no commits yet
29
+ # repo.no_commits? #=> false # at least one commit exists
30
+ #
31
+ # @return [Boolean] `true` when the repository has no commits, `false` otherwise
32
+ #
33
+ # @raise [Git::FailedError] if git exits with a non-zero exit status other
34
+ # than when the repository has no commits
35
+ #
36
+ def no_commits?
37
+ Git::Commands::RevParse.new(@execution_context).call('HEAD', verify: true)
38
+ false
39
+ rescue Git::FailedError => e
40
+ raise unless e.result.status.exitstatus == 128 &&
41
+ e.result.stderr == 'fatal: Needed a single revision'
42
+
43
+ true
44
+ end
45
+
46
+ # List all files in the working tree that are not tracked by git
47
+ #
48
+ # Runs `git ls-files --others --exclude-standard` from the working tree
49
+ # root and returns an array of repository-relative file paths. Files that
50
+ # match `.gitignore` or other standard exclusion rules are omitted.
51
+ #
52
+ # @example Get untracked files
53
+ # repo.untracked_files #=> ["new_feature.rb", "tmp/debug.log"]
54
+ #
55
+ # @example No untracked files
56
+ # repo.untracked_files #=> []
57
+ #
58
+ # @return [Array<String>] repository-relative paths of untracked,
59
+ # non-ignored files; empty when there are none
60
+ #
61
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
62
+ #
63
+ def untracked_files
64
+ Git::Commands::LsFiles.new(@execution_context).call(
65
+ others: true, exclude_standard: true, chdir: @execution_context.git_work_dir
66
+ ).stdout.split("\n").map { |f| Private.unescape_quoted_path(f) }
67
+ end
68
+
69
+ # Returns a {Git::Status} object describing the working tree and index state
70
+ #
71
+ # Constructs a {Git::Status} for this repository by collecting information from
72
+ # `git ls-files --stage`, `git ls-files --others`, `git diff-files`, and
73
+ # `git diff-index HEAD` (the last only when at least one commit exists). The
74
+ # result identifies which files have been modified, added, deleted, or are
75
+ # untracked.
76
+ #
77
+ # @example Check which files are modified
78
+ # repo.status.changed #=> { "lib/foo.rb" => <Git::Status::StatusFile ...> }
79
+ #
80
+ # @example Check for untracked files
81
+ # repo.status.untracked #=> { "new_file.rb" => <Git::Status::StatusFile ...> }
82
+ #
83
+ # @example Iterate over all status files
84
+ # repo.status.each { |file| puts "#{file.path}: #{file.type}" }
85
+ #
86
+ # @return [Git::Status] the status of the repository
87
+ #
88
+ # @raise [Git::FailedError] if any underlying git command exits with a
89
+ # non-zero exit status
90
+ #
91
+ def status
92
+ Git::Status.new(self)
93
+ end
94
+
95
+ # List all files tracked in the index
96
+ #
97
+ # Runs `git ls-files --stage` under the given `location` and returns a
98
+ # hash keyed by file path with per-file index metadata.
99
+ #
100
+ # @example List all indexed files in the working tree
101
+ # repo.ls_files
102
+ # #=> { "README.md" => { path: "README.md", mode_index: "100644",
103
+ # #=> sha_index: "abc123...", stage: "0" }, ... }
104
+ #
105
+ # @example List indexed files under a specific directory
106
+ # repo.ls_files('lib/')
107
+ # #=> { "lib/git.rb" => { path: "lib/git.rb", ... }, ... }
108
+ #
109
+ # @param location [String, nil] the path to restrict the listing to;
110
+ # defaults to `'.'` (all tracked files) when `nil`
111
+ #
112
+ # @return [Hash{String => Hash}] a hash of index entries keyed by file path
113
+ #
114
+ # Each value is a Hash with the following keys:
115
+ # * `:path` [String] the file path
116
+ # * `:mode_index` [String] the file's index mode (e.g. `"100644"`)
117
+ # * `:sha_index` [String] the file's index SHA
118
+ # * `:stage` [String] the merge stage (`"0"` for normal entries)
119
+ #
120
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
121
+ #
122
+ def ls_files(location = nil)
123
+ location ||= '.'
124
+ {}.tap do |files|
125
+ Git::Commands::LsFiles.new(@execution_context).call(location, stage: true).stdout.split("\n").each do |line|
126
+ info, file = Private.split_status_line(line)
127
+ mode, sha, stage = info.split
128
+ files[file] = { path: file, mode_index: mode, sha_index: sha, stage: stage }
129
+ end
130
+ end
131
+ end
132
+
133
+ # Private helpers local to {Git::Repository::StatusOperations}
134
+ #
135
+ # @api private
136
+ #
137
+ module Private
138
+ module_function
139
+
140
+ # Split a tab-delimited status line from `git ls-files --stage` output
141
+ #
142
+ # The output format is `<mode> <sha> <stage>\t<file>`. Quoted file paths
143
+ # (which git uses when the path contains non-ASCII or special characters)
144
+ # are unescaped before being returned. `line` is assumed to be non-empty
145
+ # because `git ls-files --stage` never emits blank lines.
146
+ #
147
+ # @param line [String] a single line of git ls-files output
148
+ #
149
+ # @return [Array<String>] the tab-delimited parts with the last part
150
+ # unescaped when it was git-quoted
151
+ #
152
+ def split_status_line(line)
153
+ parts = line.split("\t")
154
+ parts[-1] = unescape_quoted_path(parts[-1])
155
+ parts
156
+ end
157
+
158
+ # Unescape a git-quoted path
159
+ #
160
+ # Git wraps paths containing non-ASCII or special characters in
161
+ # double-quotes and octal-escapes each byte. This method strips the
162
+ # surrounding quotes and delegates unescaping to {Git::EscapedPath}.
163
+ #
164
+ # @param path [String] the path as it appears in git output
165
+ #
166
+ # @return [String] the unescaped path
167
+ #
168
+ def unescape_quoted_path(path)
169
+ if path.start_with?('"') && path.end_with?('"')
170
+ Git::EscapedPath.new(path[1..-2]).unescape
171
+ else
172
+ path
173
+ end
174
+ end
175
+ end
176
+
177
+ private_constant :Private
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/worktree'
4
+ require 'git/worktree'
5
+ require 'git/worktrees'
6
+
7
+ module Git
8
+ class Repository
9
+ # Facade methods for worktree operations
10
+ #
11
+ # Included by {Git::Repository}.
12
+ #
13
+ # @api public
14
+ #
15
+ module WorktreeOperations
16
+ # Returns all worktrees as an array of directory and SHA pairs
17
+ #
18
+ # Lists all worktrees attached to the repository, including the main
19
+ # worktree and all linked worktrees.
20
+ #
21
+ # @example List all worktrees
22
+ # repo.worktrees_all
23
+ # #=> [["/path/to/main", "4bef5ab..."], ["/tmp/worktree-1", "b8c6320..."]]
24
+ #
25
+ # @return [Array<Array(String, String)>] array of `[directory, sha]` pairs
26
+ #
27
+ # `directory` is the worktree path reported by git (absolute or relative,
28
+ # depending on repository configuration); `sha` is the full SHA of the
29
+ # checked-out HEAD commit
30
+ #
31
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
32
+ #
33
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
34
+ #
35
+ def worktrees_all
36
+ worktree_entries = []
37
+ current_directory = ''
38
+ command_output = Git::Commands::Worktree::List.new(@execution_context).call(porcelain: true).stdout
39
+
40
+ command_output.each_line(chomp: true) do |line|
41
+ key, value = line.split(' ', 2)
42
+ current_directory = value if key == 'worktree'
43
+ worktree_entries << [current_directory, value] if key == 'HEAD'
44
+ end
45
+
46
+ worktree_entries
47
+ end
48
+
49
+ # Create a new linked worktree at the given directory
50
+ #
51
+ # @example Create a worktree at a path (auto-creates a branch)
52
+ # repo.worktree_add('/tmp/feature')
53
+ #
54
+ # @example Create a worktree and check out an existing commitish
55
+ # repo.worktree_add('/tmp/hotfix', 'main')
56
+ #
57
+ # @param dir [String] filesystem path for the new worktree
58
+ #
59
+ # @param commitish [String, nil] branch, tag, or commit to check out
60
+ #
61
+ # When `nil`, git creates a new branch named after the final path component
62
+ #
63
+ # @return [String] the output from the git worktree add command
64
+ #
65
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
66
+ #
67
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
68
+ #
69
+ def worktree_add(dir, commitish = nil)
70
+ args = [dir]
71
+ args << commitish unless commitish.nil?
72
+
73
+ Git::Commands::Worktree::Add.new(@execution_context).call(*args).stdout
74
+ end
75
+
76
+ # Remove a linked worktree
77
+ #
78
+ # @example Remove a worktree
79
+ # repo.worktree_remove('/tmp/feature')
80
+ #
81
+ # @param dir [String] filesystem path of the worktree to remove
82
+ #
83
+ # @return [String] the output from the git worktree remove command
84
+ # (typically empty)
85
+ #
86
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
87
+ #
88
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
89
+ #
90
+ def worktree_remove(dir)
91
+ Git::Commands::Worktree::Remove.new(@execution_context).call(dir).stdout
92
+ end
93
+
94
+ # Prune stale worktree administrative files
95
+ #
96
+ # Removes stale administrative files from `$GIT_DIR/worktrees`. A
97
+ # worktree becomes stale when its directory no longer exists on disk.
98
+ #
99
+ # @example Prune stale worktrees
100
+ # repo.worktree_prune
101
+ #
102
+ # @return [String] the output from the git worktree prune command
103
+ # (typically empty)
104
+ #
105
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
106
+ #
107
+ # @see https://git-scm.com/docs/git-worktree git-worktree documentation
108
+ #
109
+ def worktree_prune
110
+ Git::Commands::Worktree::Prune.new(@execution_context).call.stdout
111
+ end
112
+
113
+ # Return a {Git::Worktree} object for the given directory and optional commitish
114
+ #
115
+ # This is a factory method — it constructs the domain object but does not
116
+ # immediately execute any git commands.
117
+ #
118
+ # @example Get a worktree object for a new path
119
+ # wt = repo.worktree('/tmp/feature')
120
+ #
121
+ # @example Get a worktree object for a specific branch or commit
122
+ # wt = repo.worktree('/tmp/hotfix', 'main')
123
+ #
124
+ # @param dir [String] filesystem path for the worktree
125
+ #
126
+ # @param commitish [String, nil] branch, tag, or commit to associate with
127
+ # the worktree; `nil` means no commitish is specified
128
+ #
129
+ # @return [Git::Worktree] a worktree domain object for the given path
130
+ #
131
+ def worktree(dir, commitish = nil)
132
+ Git::Worktree.new(self, dir, commitish)
133
+ end
134
+
135
+ # Return a {Git::Worktrees} collection of all worktrees (main and linked)
136
+ #
137
+ # The collection is populated eagerly when this method is called (git runs
138
+ # at construction time). It is enumerable and supports indexed access by
139
+ # worktree path.
140
+ #
141
+ # @example Iterate over all worktrees
142
+ # repo.worktrees.each { |wt| puts wt.dir }
143
+ #
144
+ # @example Count worktrees
145
+ # repo.worktrees.size
146
+ #
147
+ # @example Access a specific worktree by path
148
+ # repo.worktrees['/tmp/feature']
149
+ #
150
+ # @return [Git::Worktrees] an enumerable collection of all worktrees
151
+ #
152
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
153
+ #
154
+ def worktrees
155
+ Git::Worktrees.new(self)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -1,6 +1,269 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'find'
4
+ require 'pathname'
5
+
6
+ require 'git/execution_context/repository'
7
+ require 'git/repository/branching'
8
+ require 'git/repository/committing'
9
+ require 'git/repository/configuring'
10
+ require 'git/repository/diffing'
11
+ require 'git/repository/inspecting'
12
+ require 'git/repository/logging'
13
+ require 'git/repository/merging'
14
+ require 'git/repository/object_operations'
15
+ require 'git/repository/path_resolver'
16
+ require 'git/repository/remote_operations'
17
+ require 'git/repository/staging'
18
+ require 'git/repository/stashing'
19
+ require 'git/repository/status_operations'
20
+ require 'git/repository/worktree_operations'
21
+
3
22
  module Git
4
- class Repository < Path
23
+ # The main public interface for interacting with a Git repository
24
+ #
25
+ # `Git::Repository` is the **orchestration layer** for all git operations. It acts
26
+ # as the glue between the user-facing API and the underlying components, but
27
+ # contains minimal domain logic itself. For each operation it:
28
+ #
29
+ # 1. **Pre-processes arguments** — transforms user-provided values into forms
30
+ # suitable for the command layer (e.g. path expansion, option normalization,
31
+ # Ruby-idiomatic defaults, deprecation handling, input validation).
32
+ # 2. **Calls commands** — invokes one or more `Git::Commands::*` classes via the
33
+ # injected `Git::ExecutionContext::Repository`.
34
+ # 3. **Builds rich return values** — passes raw command output through
35
+ # `Git::Parsers::*` classes and result-class factory methods to assemble the
36
+ # meaningful Ruby objects the caller expects.
37
+ #
38
+ # Some operations are genuinely one-line delegators when no pre/post-processing is
39
+ # needed (e.g. `add`, `reset`), but many are short orchestration sequences that
40
+ # coordinate argument preparation, one or more command calls, and result assembly.
41
+ #
42
+ # Facade methods are organized into focused modules under `lib/git/repository/`
43
+ # (e.g. {Git::Repository::Staging}) and included into this class.
44
+ #
45
+ # @api public
46
+ #
47
+ class Repository
48
+ include Git::Repository::Branching
49
+ include Git::Repository::Committing
50
+ include Git::Repository::Configuring
51
+ include Git::Repository::Diffing
52
+ include Git::Repository::Inspecting
53
+ include Git::Repository::Logging
54
+ include Git::Repository::Merging
55
+ include Git::Repository::ObjectOperations
56
+ include Git::Repository::RemoteOperations
57
+ include Git::Repository::Staging
58
+ include Git::Repository::Stashing
59
+ include Git::Repository::StatusOperations
60
+ include Git::Repository::WorktreeOperations
61
+
62
+ # Open a working copy at an existing path
63
+ #
64
+ # The new repository factories are additive scaffolding introduced by the
65
+ # architectural redesign. The top-level {Git.open} entry point still returns a
66
+ # {Git::Base} object; this method exists so future work can route construction
67
+ # through {Git::Repository} without changing the public entry points.
68
+ #
69
+ # Note: this method opens working copies only. To open a bare repository, use
70
+ # {Git::Repository.bare}.
71
+ #
72
+ # @example Open the working copy in the current directory
73
+ # repository = Git::Repository.open('.')
74
+ #
75
+ # @param working_dir [String] the path to the root of the working copy
76
+ #
77
+ # May be any path inside the working tree when `:repository` is not given.
78
+ #
79
+ # @param options [Hash] options that control how the repository is located
80
+ #
81
+ # @option options [String] :repository a non-standard path to the `.git`
82
+ # directory
83
+ #
84
+ # When given, `working_dir` is used as-is (the working tree root is not
85
+ # auto-detected).
86
+ #
87
+ # @option options [String] :index a non-standard path to the index file
88
+ #
89
+ # @option options [Logger] :log a logger forwarded to the command layer
90
+ #
91
+ # @option options [String, nil, :use_global_config] :git_ssh path to a custom SSH executable;
92
+ # pass `:use_global_config` (the default) to use `Git::Base.config.git_ssh`
93
+ #
94
+ # @option options [String, :use_global_config] :binary_path path to the git binary;
95
+ # pass `:use_global_config` (the default) to use `Git::Base.config.binary_path`
96
+ #
97
+ # @return [Git::Repository] a repository bound to the resolved paths
98
+ #
99
+ # @raise [ArgumentError] if `working_dir` is not a directory or is not inside
100
+ # a git working tree
101
+ #
102
+ def self.open(working_dir, options = {})
103
+ raise ArgumentError, "'#{working_dir}' is not a directory" unless Dir.exist?(working_dir)
104
+
105
+ working_dir = resolve_open_working_dir(working_dir, options) unless options[:repository]
106
+
107
+ paths = PathResolver.resolve_paths(
108
+ working_directory: working_dir,
109
+ repository: options[:repository],
110
+ index: options[:index]
111
+ )
112
+
113
+ from_paths(options, paths)
114
+ end
115
+
116
+ # Open an existing bare repository at `git_dir`
117
+ #
118
+ # The new repository factories are additive scaffolding introduced by the
119
+ # architectural redesign. The top-level {Git.bare} entry point still returns a
120
+ # {Git::Base} object; this method exists so future work can route construction
121
+ # through {Git::Repository} without changing the public entry points.
122
+ #
123
+ # @example Open a bare repository
124
+ # repository = Git::Repository.bare('/path/to/repo.git')
125
+ #
126
+ # @param git_dir [String] the path to the bare repository directory
127
+ #
128
+ # @param options [Hash] options forwarded to the constructed repository
129
+ #
130
+ # @option options [Logger] :log a logger forwarded to the command layer
131
+ #
132
+ # @option options [String, nil, :use_global_config] :git_ssh path to a custom SSH executable;
133
+ # pass `:use_global_config` (the default) to use `Git::Base.config.git_ssh`
134
+ #
135
+ # @option options [String, :use_global_config] :binary_path path to the git binary;
136
+ # pass `:use_global_config` (the default) to use `Git::Base.config.binary_path`
137
+ #
138
+ # @return [Git::Repository] a repository bound to the bare repository directory
139
+ #
140
+ def self.bare(git_dir, options = {})
141
+ paths = PathResolver.resolve_paths(repository: git_dir, bare: true)
142
+
143
+ from_paths(options, paths)
144
+ end
145
+
146
+ # Resolve the worktree root to use as the working directory for `.open`
147
+ #
148
+ # Delegates to {PathResolver.root_of_worktree}, forwarding `:binary_path`
149
+ # and `:git_ssh` from `options`.
150
+ #
151
+ # @param working_dir [String] a path inside the working tree
152
+ #
153
+ # @param options [Hash] the caller-supplied options hash from `.open`
154
+ #
155
+ # @return [String] the absolute path to the root of the working tree
156
+ #
157
+ # @raise [ArgumentError] if `working_dir` is not inside a git working tree
158
+ #
159
+ # @api private
160
+ #
161
+ def self.resolve_open_working_dir(working_dir, options)
162
+ PathResolver.root_of_worktree(
163
+ working_dir,
164
+ binary_path: options.fetch(:binary_path, :use_global_config),
165
+ git_ssh: options.fetch(:git_ssh, :use_global_config)
166
+ )
167
+ end
168
+ private_class_method :resolve_open_working_dir
169
+
170
+ # Build a repository from caller options and resolved paths
171
+ #
172
+ # @param options [Hash] the caller-supplied options (`:git_ssh`,
173
+ # `:binary_path`, `:log`)
174
+ #
175
+ # @param paths [Hash{Symbol => (String, nil)}] the resolved
176
+ # `:working_directory`, `:repository`, and `:index` paths
177
+ #
178
+ # @return [Git::Repository] the constructed repository
179
+ #
180
+ # @api private
181
+ #
182
+ def self.from_paths(options, paths)
183
+ execution_context = Git::ExecutionContext::Repository.from_hash(
184
+ options.merge(paths), logger: options[:log]
185
+ )
186
+ new(execution_context: execution_context)
187
+ end
188
+ private_class_method :from_paths
189
+
190
+ # @return [Git::ExecutionContext::Repository] the execution context used to run
191
+ # git commands for this repository
192
+ # @api private
193
+ attr_reader :execution_context
194
+
195
+ # @param execution_context [Git::ExecutionContext::Repository] the context used
196
+ # to run git commands for this repository; must not be nil
197
+ #
198
+ # @raise [ArgumentError] if `execution_context` is nil
199
+ #
200
+ def initialize(execution_context:)
201
+ raise ArgumentError, 'execution_context must not be nil' if execution_context.nil?
202
+
203
+ @execution_context = execution_context
204
+ end
205
+
206
+ # Returns the root of the working tree, or `nil` for a bare repository
207
+ #
208
+ # @example Get the working directory path
209
+ # repository.dir #=> #<Pathname:/path/to/repo>
210
+ #
211
+ # @return [Pathname, nil] the working directory path, or `nil` when bare
212
+ #
213
+ def dir
214
+ working_dir = execution_context.git_work_dir
215
+ working_dir && Pathname.new(working_dir)
216
+ end
217
+
218
+ # Returns the repository (`.git`) directory
219
+ #
220
+ # @example Get the repository directory path
221
+ # repository.repo #=> #<Pathname:/path/to/repo/.git>
222
+ #
223
+ # @return [Pathname, nil] the repository directory path
224
+ #
225
+ def repo
226
+ repository = execution_context.git_dir
227
+ repository && Pathname.new(repository)
228
+ end
229
+
230
+ # Returns the git index file
231
+ #
232
+ # @example Get the index file path
233
+ # repository.index #=> #<Pathname:/path/to/repo/.git/index>
234
+ #
235
+ # @return [Pathname, nil] the index file path
236
+ #
237
+ def index
238
+ index_file = execution_context.git_index_file
239
+ index_file && Pathname.new(index_file)
240
+ end
241
+
242
+ # Returns the size of the repository directory in bytes
243
+ #
244
+ # Sums the sizes of every regular file under the repository (`.git`)
245
+ # directory in a single traversal. Symbolic links are not followed, so files
246
+ # that physically live outside the repository (reached through a symlinked
247
+ # directory) are never counted. Files that disappear mid-traversal are
248
+ # silently skipped.
249
+ #
250
+ # @example Get the repository size in bytes
251
+ # repository.repo_size #=> 12345
252
+ #
253
+ # @return [Integer] the total size in bytes of the repository directory
254
+ #
255
+ def repo_size
256
+ repository = repo
257
+ return 0 unless repository&.directory?
258
+
259
+ total = 0
260
+ Find.find(repository.to_s) do |path|
261
+ stat = File.lstat(path)
262
+ total += stat.size if stat.file?
263
+ rescue Errno::ENOENT
264
+ next
265
+ end
266
+ total
267
+ end
5
268
  end
6
269
  end