git 4.3.2 → 5.0.0.beta.2

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 (297) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +73 -2705
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/pull_request_template.md +3 -1
  5. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  6. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  7. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  8. data/.github/skills/command-implementation/SKILL.md +229 -0
  9. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  10. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  11. data/.github/skills/dependency-management/SKILL.md +72 -0
  12. data/.github/skills/development-workflow/SKILL.md +506 -0
  13. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  14. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  15. data/.github/skills/extract-facade-from-base-lib/SKILL.md +600 -0
  16. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  17. data/.github/skills/facade-implementation/SKILL.md +274 -0
  18. data/.github/skills/facade-test-conventions/SKILL.md +394 -0
  19. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  20. data/.github/skills/make-skill-template/SKILL.md +176 -0
  21. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  22. data/.github/skills/project-context/SKILL.md +313 -0
  23. data/.github/skills/pull-request-review/SKILL.md +168 -0
  24. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  25. data/.github/skills/release-management/SKILL.md +125 -0
  26. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  27. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  28. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  29. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  30. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  31. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  32. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  33. data/.github/skills/test-debugging/SKILL.md +160 -0
  34. data/.github/skills/yard-documentation/SKILL.md +793 -0
  35. data/.github/workflows/continuous_integration.yml +3 -2
  36. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  37. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  38. data/.github/workflows/release.yml +3 -4
  39. data/.gitignore +8 -0
  40. data/.husky/pre-commit +13 -0
  41. data/.release-please-manifest.json +1 -1
  42. data/.rspec +3 -0
  43. data/.rubocop.yml +12 -3
  44. data/.rubocop_todo.yml +23 -5
  45. data/.yardopts +1 -0
  46. data/CHANGELOG.md +0 -40
  47. data/CONTRIBUTING.md +694 -53
  48. data/README.md +67 -15
  49. data/Rakefile +61 -9
  50. data/UPGRADING.md +141 -0
  51. data/commitlint.test +4 -0
  52. data/git.gemspec +18 -7
  53. data/lib/git/args_builder.rb +0 -8
  54. data/lib/git/branch.rb +369 -43
  55. data/lib/git/branch_delete_failure.rb +31 -0
  56. data/lib/git/branch_delete_result.rb +63 -0
  57. data/lib/git/branch_info.rb +178 -0
  58. data/lib/git/branches.rb +122 -24
  59. data/lib/git/command_line/base.rb +255 -0
  60. data/lib/git/command_line/capturing.rb +251 -0
  61. data/lib/git/command_line/result.rb +96 -0
  62. data/lib/git/command_line/streaming.rb +196 -0
  63. data/lib/git/command_line.rb +43 -322
  64. data/lib/git/command_line_result.rb +4 -88
  65. data/lib/git/commands/add.rb +131 -0
  66. data/lib/git/commands/am/abort.rb +43 -0
  67. data/lib/git/commands/am/apply.rb +252 -0
  68. data/lib/git/commands/am/continue.rb +43 -0
  69. data/lib/git/commands/am/quit.rb +43 -0
  70. data/lib/git/commands/am/retry.rb +47 -0
  71. data/lib/git/commands/am/show_current_patch.rb +64 -0
  72. data/lib/git/commands/am/skip.rb +42 -0
  73. data/lib/git/commands/am.rb +33 -0
  74. data/lib/git/commands/apply.rb +237 -0
  75. data/lib/git/commands/archive/list_formats.rb +46 -0
  76. data/lib/git/commands/archive.rb +140 -0
  77. data/lib/git/commands/arguments.rb +3510 -0
  78. data/lib/git/commands/base.rb +404 -0
  79. data/lib/git/commands/branch/copy.rb +94 -0
  80. data/lib/git/commands/branch/create.rb +173 -0
  81. data/lib/git/commands/branch/delete.rb +80 -0
  82. data/lib/git/commands/branch/list.rb +162 -0
  83. data/lib/git/commands/branch/move.rb +94 -0
  84. data/lib/git/commands/branch/set_upstream.rb +86 -0
  85. data/lib/git/commands/branch/show_current.rb +49 -0
  86. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  87. data/lib/git/commands/branch.rb +34 -0
  88. data/lib/git/commands/cat_file/batch.rb +369 -0
  89. data/lib/git/commands/cat_file/filtered.rb +105 -0
  90. data/lib/git/commands/cat_file/raw.rb +216 -0
  91. data/lib/git/commands/cat_file.rb +49 -0
  92. data/lib/git/commands/checkout/branch.rb +151 -0
  93. data/lib/git/commands/checkout/files.rb +115 -0
  94. data/lib/git/commands/checkout.rb +38 -0
  95. data/lib/git/commands/checkout_index.rb +105 -0
  96. data/lib/git/commands/clean.rb +100 -0
  97. data/lib/git/commands/clone.rb +240 -0
  98. data/lib/git/commands/commit.rb +272 -0
  99. data/lib/git/commands/commit_tree.rb +100 -0
  100. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  101. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  102. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  103. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  104. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  105. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  106. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +107 -0
  107. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  108. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  109. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  110. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  111. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  112. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  113. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  114. data/lib/git/commands/config_option_syntax.rb +56 -0
  115. data/lib/git/commands/describe.rb +155 -0
  116. data/lib/git/commands/diff.rb +656 -0
  117. data/lib/git/commands/diff_files.rb +518 -0
  118. data/lib/git/commands/diff_index.rb +496 -0
  119. data/lib/git/commands/fetch.rb +352 -0
  120. data/lib/git/commands/fsck.rb +136 -0
  121. data/lib/git/commands/gc.rb +132 -0
  122. data/lib/git/commands/grep.rb +338 -0
  123. data/lib/git/commands/init.rb +99 -0
  124. data/lib/git/commands/log.rb +632 -0
  125. data/lib/git/commands/ls_files.rb +191 -0
  126. data/lib/git/commands/ls_remote.rb +155 -0
  127. data/lib/git/commands/ls_tree.rb +131 -0
  128. data/lib/git/commands/maintenance/register.rb +75 -0
  129. data/lib/git/commands/maintenance/run.rb +104 -0
  130. data/lib/git/commands/maintenance/start.rb +66 -0
  131. data/lib/git/commands/maintenance/stop.rb +55 -0
  132. data/lib/git/commands/maintenance/unregister.rb +79 -0
  133. data/lib/git/commands/maintenance.rb +31 -0
  134. data/lib/git/commands/merge/abort.rb +44 -0
  135. data/lib/git/commands/merge/continue.rb +44 -0
  136. data/lib/git/commands/merge/quit.rb +46 -0
  137. data/lib/git/commands/merge/start.rb +245 -0
  138. data/lib/git/commands/merge.rb +28 -0
  139. data/lib/git/commands/merge_base.rb +86 -0
  140. data/lib/git/commands/mv.rb +77 -0
  141. data/lib/git/commands/name_rev.rb +114 -0
  142. data/lib/git/commands/pull.rb +377 -0
  143. data/lib/git/commands/push.rb +246 -0
  144. data/lib/git/commands/read_tree.rb +149 -0
  145. data/lib/git/commands/remote/add.rb +91 -0
  146. data/lib/git/commands/remote/get_url.rb +66 -0
  147. data/lib/git/commands/remote/list.rb +54 -0
  148. data/lib/git/commands/remote/prune.rb +61 -0
  149. data/lib/git/commands/remote/remove.rb +52 -0
  150. data/lib/git/commands/remote/rename.rb +69 -0
  151. data/lib/git/commands/remote/set_branches.rb +63 -0
  152. data/lib/git/commands/remote/set_head.rb +82 -0
  153. data/lib/git/commands/remote/set_url.rb +71 -0
  154. data/lib/git/commands/remote/set_url_add.rb +61 -0
  155. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  156. data/lib/git/commands/remote/show.rb +71 -0
  157. data/lib/git/commands/remote/update.rb +72 -0
  158. data/lib/git/commands/remote.rb +42 -0
  159. data/lib/git/commands/repack.rb +277 -0
  160. data/lib/git/commands/reset.rb +147 -0
  161. data/lib/git/commands/rev_parse.rb +297 -0
  162. data/lib/git/commands/revert/abort.rb +45 -0
  163. data/lib/git/commands/revert/continue.rb +57 -0
  164. data/lib/git/commands/revert/quit.rb +47 -0
  165. data/lib/git/commands/revert/skip.rb +44 -0
  166. data/lib/git/commands/revert/start.rb +153 -0
  167. data/lib/git/commands/revert.rb +29 -0
  168. data/lib/git/commands/rm.rb +114 -0
  169. data/lib/git/commands/show.rb +632 -0
  170. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  171. data/lib/git/commands/show_ref/exists.rb +78 -0
  172. data/lib/git/commands/show_ref/list.rb +145 -0
  173. data/lib/git/commands/show_ref/verify.rb +120 -0
  174. data/lib/git/commands/show_ref.rb +42 -0
  175. data/lib/git/commands/stash/apply.rb +75 -0
  176. data/lib/git/commands/stash/branch.rb +65 -0
  177. data/lib/git/commands/stash/clear.rb +41 -0
  178. data/lib/git/commands/stash/create.rb +58 -0
  179. data/lib/git/commands/stash/drop.rb +67 -0
  180. data/lib/git/commands/stash/list.rb +39 -0
  181. data/lib/git/commands/stash/pop.rb +78 -0
  182. data/lib/git/commands/stash/push.rb +103 -0
  183. data/lib/git/commands/stash/show.rb +149 -0
  184. data/lib/git/commands/stash/store.rb +63 -0
  185. data/lib/git/commands/stash.rb +38 -0
  186. data/lib/git/commands/status.rb +169 -0
  187. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  188. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  189. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  190. data/lib/git/commands/symbolic_ref.rb +38 -0
  191. data/lib/git/commands/tag/create.rb +139 -0
  192. data/lib/git/commands/tag/delete.rb +55 -0
  193. data/lib/git/commands/tag/list.rb +143 -0
  194. data/lib/git/commands/tag/verify.rb +71 -0
  195. data/lib/git/commands/tag.rb +26 -0
  196. data/lib/git/commands/update_ref/batch.rb +140 -0
  197. data/lib/git/commands/update_ref/delete.rb +92 -0
  198. data/lib/git/commands/update_ref/update.rb +106 -0
  199. data/lib/git/commands/update_ref.rb +42 -0
  200. data/lib/git/commands/version.rb +57 -0
  201. data/lib/git/commands/worktree/add.rb +140 -0
  202. data/lib/git/commands/worktree/list.rb +64 -0
  203. data/lib/git/commands/worktree/lock.rb +58 -0
  204. data/lib/git/commands/worktree/management_base.rb +51 -0
  205. data/lib/git/commands/worktree/move.rb +66 -0
  206. data/lib/git/commands/worktree/prune.rb +67 -0
  207. data/lib/git/commands/worktree/remove.rb +63 -0
  208. data/lib/git/commands/worktree/repair.rb +76 -0
  209. data/lib/git/commands/worktree/unlock.rb +47 -0
  210. data/lib/git/commands/worktree.rb +43 -0
  211. data/lib/git/commands/write_tree.rb +68 -0
  212. data/lib/git/commands.rb +87 -0
  213. data/lib/git/config.rb +17 -0
  214. data/lib/git/config_entry_info.rb +106 -0
  215. data/lib/git/configuring.rb +665 -0
  216. data/lib/git/deprecation.rb +9 -0
  217. data/lib/git/detached_head_info.rb +54 -0
  218. data/lib/git/diff.rb +293 -7
  219. data/lib/git/diff_file_numstat_info.rb +29 -0
  220. data/lib/git/diff_file_patch_info.rb +134 -0
  221. data/lib/git/diff_file_raw_info.rb +127 -0
  222. data/lib/git/diff_info.rb +169 -0
  223. data/lib/git/diff_path_status.rb +67 -19
  224. data/lib/git/diff_result.rb +32 -0
  225. data/lib/git/diff_stats.rb +51 -14
  226. data/lib/git/dirstat_info.rb +86 -0
  227. data/lib/git/errors.rb +65 -2
  228. data/lib/git/execution_context/global.rb +31 -0
  229. data/lib/git/execution_context/repository.rb +136 -0
  230. data/lib/git/execution_context.rb +501 -0
  231. data/lib/git/file_ref.rb +74 -0
  232. data/lib/git/fsck_object.rb +9 -9
  233. data/lib/git/fsck_result.rb +1 -1
  234. data/lib/git/log.rb +9 -2
  235. data/lib/git/object.rb +90 -27
  236. data/lib/git/parsers/branch.rb +224 -0
  237. data/lib/git/parsers/cat_file.rb +111 -0
  238. data/lib/git/parsers/config_entry.rb +110 -0
  239. data/lib/git/parsers/diff.rb +585 -0
  240. data/lib/git/parsers/fsck.rb +133 -0
  241. data/lib/git/parsers/grep.rb +42 -0
  242. data/lib/git/parsers/ls_remote.rb +79 -0
  243. data/lib/git/parsers/ls_tree.rb +58 -0
  244. data/lib/git/parsers/stash.rb +208 -0
  245. data/lib/git/parsers/tag.rb +257 -0
  246. data/lib/git/remote.rb +120 -9
  247. data/lib/git/repository/branching.rb +743 -0
  248. data/lib/git/repository/committing.rb +187 -0
  249. data/lib/git/repository/configuring.rb +351 -0
  250. data/lib/git/repository/context_helpers.rb +264 -0
  251. data/lib/git/repository/diffing.rb +775 -0
  252. data/lib/git/repository/factories.rb +682 -0
  253. data/lib/git/repository/inspecting.rb +252 -0
  254. data/lib/git/repository/logging.rb +247 -0
  255. data/lib/git/repository/maintenance.rb +65 -0
  256. data/lib/git/repository/merging.rb +357 -0
  257. data/lib/git/repository/object_operations.rb +1199 -0
  258. data/lib/git/repository/path_resolver.rb +207 -0
  259. data/lib/git/repository/remote_operations.rb +898 -0
  260. data/lib/git/repository/shared_private.rb +51 -0
  261. data/lib/git/repository/staging.rb +554 -0
  262. data/lib/git/repository/stashing.rb +143 -0
  263. data/lib/git/repository/status_operations.rb +201 -0
  264. data/lib/git/repository/worktree_operations.rb +159 -0
  265. data/lib/git/repository.rb +203 -1
  266. data/lib/git/stash.rb +78 -4
  267. data/lib/git/stash_info.rb +104 -0
  268. data/lib/git/stashes.rb +125 -13
  269. data/lib/git/status.rb +215 -18
  270. data/lib/git/tag_delete_failure.rb +31 -0
  271. data/lib/git/tag_delete_result.rb +63 -0
  272. data/lib/git/tag_info.rb +105 -0
  273. data/lib/git/version.rb +109 -2
  274. data/lib/git/version_constraint.rb +81 -0
  275. data/lib/git/worktree.rb +108 -6
  276. data/lib/git/worktrees.rb +94 -7
  277. data/lib/git.rb +260 -61
  278. data/redesign/1_architecture_existing.md +54 -18
  279. data/redesign/2_architecture_redesign.md +365 -46
  280. data/redesign/3_architecture_implementation.md +1497 -63
  281. data/redesign/Phase 4 - Step A.md +360 -0
  282. data/redesign/beta_release.md +107 -0
  283. data/redesign/c1c2_audit.md +566 -0
  284. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  285. data/redesign/config_design.rb +501 -0
  286. data/tasks/gem_tasks.rake +4 -0
  287. data/tasks/npm_tasks.rake +7 -0
  288. data/tasks/rspec.rake +48 -0
  289. data/tasks/test.rake +13 -1
  290. data/tasks/yard.rake +34 -7
  291. metadata +365 -22
  292. data/lib/git/base.rb +0 -1128
  293. data/lib/git/index.rb +0 -6
  294. data/lib/git/lib.rb +0 -2277
  295. data/lib/git/path.rb +0 -38
  296. data/lib/git/working_directory.rb +0 -6
  297. /data/{release-please-config.json → .release-please-config.json} +0 -0
data/lib/git/lib.rb DELETED
@@ -1,2277 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'args_builder'
4
-
5
- require 'git/command_line'
6
- require 'git/errors'
7
- require 'logger'
8
- require 'pathname'
9
- require 'pp'
10
- require 'process_executer'
11
- require 'stringio'
12
- require 'tempfile'
13
- require 'zlib'
14
- require 'open3'
15
-
16
- module Git
17
- # Internal git operations
18
- # @api private
19
- class Lib
20
- # The path to the Git working copy. The default is '"./.git"'.
21
- #
22
- # @return [Pathname] the path to the Git working copy.
23
- #
24
- # @see [Git working tree](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefworkingtreeaworkingtree)
25
- #
26
- attr_reader :git_work_dir
27
-
28
- # The path to the Git repository directory. The default is
29
- # `"#{git_work_dir}/.git"`.
30
- #
31
- # @return [Pathname] the Git repository directory.
32
- #
33
- # @see [Git repository](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefrepositoryarepository)
34
- #
35
- attr_reader :git_dir
36
-
37
- # The Git index file used to stage changes (using `git add`) before they
38
- # are committed.
39
- #
40
- # @return [Pathname] the Git index file
41
- #
42
- # @see [Git index file](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefindexaindex)
43
- #
44
- attr_reader :git_index_file
45
-
46
- # Create a new Git::Lib object
47
- #
48
- # @overload initialize(base, logger)
49
- #
50
- # @param base [Hash] the hash containing paths to the Git working copy,
51
- # the Git repository directory, and the Git index file.
52
- #
53
- # @option base [Pathname] :working_directory
54
- # @option base [Pathname] :repository
55
- # @option base [Pathname] :index
56
- #
57
- # @param [Logger] logger
58
- #
59
- # @overload initialize(base, logger)
60
- #
61
- # @param base [#dir, #repo, #index] an object with methods to get the Git worktree (#dir),
62
- # the Git repository directory (#repo), and the Git index file (#index).
63
- #
64
- # @param [Logger] logger
65
- #
66
- def initialize(base = nil, logger = nil)
67
- @logger = logger || Logger.new(nil)
68
- @git_ssh = :use_global_config
69
-
70
- case base
71
- when Git::Base
72
- initialize_from_base(base)
73
- when Hash
74
- initialize_from_hash(base)
75
- end
76
- end
77
-
78
- INIT_OPTION_MAP = [
79
- { keys: [:bare], flag: '--bare', type: :boolean },
80
- { keys: [:initial_branch], flag: '--initial-branch', type: :valued_equals }
81
- ].freeze
82
-
83
- # creates or reinitializes the repository
84
- #
85
- # options:
86
- # :bare
87
- # :working_directory
88
- # :initial_branch
89
- #
90
- def init(opts = {})
91
- args = build_args(opts, INIT_OPTION_MAP)
92
- command('init', *args)
93
- end
94
-
95
- CLONE_OPTION_MAP = [
96
- { keys: [:bare], flag: '--bare', type: :boolean },
97
- { keys: [:recursive], flag: '--recursive', type: :boolean },
98
- { keys: [:mirror], flag: '--mirror', type: :boolean },
99
- { keys: [:branch], flag: '--branch', type: :valued_space },
100
- { keys: [:filter], flag: '--filter', type: :valued_space },
101
- { keys: %i[remote origin], flag: '--origin', type: :valued_space },
102
- { keys: [:config], flag: '--config', type: :repeatable_valued_space },
103
- {
104
- keys: [:single_branch],
105
- type: :custom,
106
- validator: ->(value) { [nil, true, false].include?(value) },
107
- builder: lambda do |value|
108
- case value
109
- when true
110
- ['--single-branch']
111
- when false
112
- ['--no-single-branch']
113
- else
114
- []
115
- end
116
- end
117
- },
118
- {
119
- keys: [:depth],
120
- type: :custom,
121
- builder: ->(value) { ['--depth', value.to_i] if value }
122
- }
123
- ].freeze
124
-
125
- # Clones a repository into a newly created directory
126
- #
127
- # @param [String] repository_url the URL of the repository to clone
128
- #
129
- # @param [String, nil] directory the directory to clone into
130
- #
131
- # If nil, the repository is cloned into a directory with the same name as
132
- # the repository.
133
- #
134
- # @param [Hash] opts the options for this command
135
- #
136
- # @option opts [Boolean] :bare (false) if true, clone as a bare repository
137
- #
138
- # @option opts [String] :branch the branch to checkout
139
- #
140
- # @option opts [String, Array] :config one or more configuration options to set
141
- #
142
- # @option opts [Integer] :depth the number of commits back to pull
143
- #
144
- # @option opts [String] :filter specify partial clone
145
- #
146
- # @option opts [String] :mirror set up a mirror of the source repository
147
- #
148
- # @option opts [String] :origin the name of the remote
149
- #
150
- # @option opts [String] :path an optional prefix for the directory parameter
151
- #
152
- # @option opts [String] :remote the name of the remote
153
- #
154
- # @option opts [Boolean] :recursive after the clone is created, initialize all
155
- # within, using their default settings
156
- #
157
- # @option opts [Numeric, nil] :timeout the number of seconds to wait for the
158
- # command to complete
159
- #
160
- # See {Git::Lib#command} for more information about :timeout
161
- #
162
- # @return [Hash] the options to pass to {Git::Base.new}
163
- #
164
- # @todo make this work with SSH password or auth_key
165
- #
166
- def clone(repository_url, directory, opts = {})
167
- @path = opts[:path] || '.'
168
- clone_dir = opts[:path] ? File.join(@path, directory) : directory
169
-
170
- args = build_args(opts, CLONE_OPTION_MAP)
171
- args.push('--', repository_url, clone_dir)
172
-
173
- command('clone', *args, timeout: opts[:timeout])
174
-
175
- return_base_opts_from_clone(clone_dir, opts)
176
- end
177
-
178
- # Returns the name of the default branch of the given repository
179
- #
180
- # @param repository [URI, Pathname, String] The (possibly remote) repository to clone from
181
- #
182
- # @return [String] the name of the default branch
183
- #
184
- def repository_default_branch(repository)
185
- output = command('ls-remote', '--symref', '--', repository, 'HEAD')
186
-
187
- match_data = output.match(%r{^ref: refs/remotes/origin/(?<default_branch>[^\t]+)\trefs/remotes/origin/HEAD$})
188
- return match_data[:default_branch] if match_data
189
-
190
- match_data = output.match(%r{^ref: refs/heads/(?<default_branch>[^\t]+)\tHEAD$})
191
- return match_data[:default_branch] if match_data
192
-
193
- raise Git::UnexpectedResultError, 'Unable to determine the default branch'
194
- end
195
-
196
- ## READ COMMANDS ##
197
-
198
- # The map defining how to translate user options to git command arguments.
199
- DESCRIBE_OPTION_MAP = [
200
- { keys: [:all], flag: '--all', type: :boolean },
201
- { keys: [:tags], flag: '--tags', type: :boolean },
202
- { keys: [:contains], flag: '--contains', type: :boolean },
203
- { keys: [:debug], flag: '--debug', type: :boolean },
204
- { keys: [:long], flag: '--long', type: :boolean },
205
- { keys: [:always], flag: '--always', type: :boolean },
206
- { keys: %i[exact_match exact-match], flag: '--exact-match', type: :boolean },
207
- { keys: [:abbrev], flag: '--abbrev', type: :valued_equals },
208
- { keys: [:candidates], flag: '--candidates', type: :valued_equals },
209
- { keys: [:match], flag: '--match', type: :valued_equals },
210
- {
211
- keys: [:dirty],
212
- type: :custom,
213
- builder: lambda do |value|
214
- return '--dirty' if value == true
215
-
216
- "--dirty=#{value}" if value.is_a?(String)
217
- end
218
- }
219
- ].freeze
220
-
221
- # Finds most recent tag that is reachable from a commit
222
- #
223
- # @see https://git-scm.com/docs/git-describe git-describe
224
- #
225
- # @param commit_ish [String, nil] target commit sha or object name
226
- #
227
- # @param opts [Hash] the given options
228
- #
229
- # @option opts :all [Boolean]
230
- # @option opts :tags [Boolean]
231
- # @option opts :contains [Boolean]
232
- # @option opts :debug [Boolean]
233
- # @option opts :long [Boolean]
234
- # @option opts :always [Boolean]
235
- # @option opts :exact_match [Boolean]
236
- # @option opts :dirty [true, String]
237
- # @option opts :abbrev [String]
238
- # @option opts :candidates [String]
239
- # @option opts :match [String]
240
- #
241
- # @return [String] the tag name
242
- #
243
- # @raise [ArgumentError] if the commit_ish is a string starting with a hyphen
244
- #
245
- def describe(commit_ish = nil, opts = {})
246
- assert_args_are_not_options('commit-ish object', commit_ish)
247
-
248
- args = build_args(opts, DESCRIBE_OPTION_MAP)
249
- args << commit_ish if commit_ish
250
-
251
- command('describe', *args)
252
- end
253
-
254
- # Return the commits that are within the given revision range
255
- #
256
- # @see https://git-scm.com/docs/git-log git-log
257
- #
258
- # @param opts [Hash] the given options
259
- #
260
- # @option opts :count [Integer] the maximum number of commits to return (maps to max-count)
261
- # @option opts :all [Boolean]
262
- # @option opts :cherry [Boolean]
263
- # @option opts :since [String]
264
- # @option opts :until [String]
265
- # @option opts :grep [String]
266
- # @option opts :author [String]
267
- # @option opts :between [Array<String>] an array of two commit-ish strings to specify a revision range
268
- #
269
- # Only :between or :object options can be used, not both.
270
- #
271
- # @option opts :object [String] the revision range for the git log command
272
- #
273
- # Only :between or :object options can be used, not both.
274
- #
275
- # @option opts :path_limiter [String, Pathname, Array<String, Pathname>] only
276
- # include commits that impact files from the specified paths
277
- #
278
- # @return [Array<String>] the log output
279
- #
280
- # @raise [ArgumentError] if the resulting revision range is a string starting with a hyphen
281
- #
282
- def log_commits(opts = {})
283
- assert_args_are_not_options('between', opts[:between]&.first)
284
- assert_args_are_not_options('object', opts[:object])
285
-
286
- arr_opts = log_common_options(opts)
287
-
288
- arr_opts << '--pretty=oneline'
289
-
290
- arr_opts += log_path_options(opts)
291
-
292
- log_or_empty_on_unborn { command_lines('log', *arr_opts).map { |l| l.split.first } }
293
- end
294
-
295
- FULL_LOG_EXTRA_OPTIONS_MAP = [
296
- { type: :static, flag: '--pretty=raw' },
297
- { keys: [:skip], flag: '--skip', type: :valued_equals },
298
- { keys: [:merges], flag: '--merges', type: :boolean }
299
- ].freeze
300
-
301
- # Return the commits that are within the given revision range
302
- #
303
- # @see https://git-scm.com/docs/git-log git-log
304
- #
305
- # @param opts [Hash] the given options
306
- #
307
- # @option opts :count [Integer] the maximum number of commits to return (maps to
308
- # max-count)
309
- #
310
- # @option opts :all [Boolean]
311
- #
312
- # @option opts :cherry [Boolean]
313
- #
314
- # @option opts :since [String]
315
- #
316
- # @option opts :until [String]
317
- #
318
- # @option opts :grep [String]
319
- #
320
- # @option opts :author [String]
321
- #
322
- # @option opts :between [Array<String>] an array of two commit-ish strings to
323
- # specify a revision range
324
- #
325
- # Only :between or :object options can be used, not both.
326
- #
327
- # @option opts :object [String] the revision range for the git log command
328
- #
329
- # Only :between or :object options can be used, not both.
330
- #
331
- # @option opts :path_limiter [String, Pathname, Array<String, Pathname>] only include commits that
332
- # impact files from the specified paths
333
- #
334
- # @option opts :skip [Integer]
335
- #
336
- # @return [Array<Hash>] the log output parsed into an array of hashs for each commit
337
- #
338
- # Each hash contains the following keys:
339
- #
340
- # * 'sha' [String] the commit sha
341
- # * 'author' [String] the author of the commit
342
- # * 'message' [String] the commit message
343
- # * 'parent' [Array<String>] the commit shas of the parent commits
344
- # * 'tree' [String] the tree sha
345
- # * 'author' [String] the author of the commit and timestamp of when the
346
- # changes were created
347
- # * 'committer' [String] the committer of the commit and timestamp of when the
348
- # commit was applied
349
- # * 'merges' [Boolean] if truthy, only include merge commits (aka commits with
350
- # 2 or more parents)
351
- #
352
- # @raise [ArgumentError] if the revision range (specified with :between or
353
- # :object) is a string starting with a hyphen
354
- #
355
- def full_log_commits(opts = {})
356
- assert_args_are_not_options('between', opts[:between]&.first)
357
- assert_args_are_not_options('object', opts[:object])
358
-
359
- args = log_common_options(opts)
360
- args += build_args(opts, FULL_LOG_EXTRA_OPTIONS_MAP)
361
- args += log_path_options(opts)
362
-
363
- log_or_empty_on_unborn do
364
- full_log = command_lines('log', *args)
365
- process_commit_log_data(full_log)
366
- end
367
- end
368
-
369
- # Verify and resolve a Git revision to its full SHA
370
- #
371
- # @see https://git-scm.com/docs/git-rev-parse git-rev-parse
372
- # @see https://git-scm.com/docs/git-rev-parse#_specifying_revisions Valid ways to specify revisions
373
- # @see https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt-emltrefnamegtemegemmasterememheadsmasterememrefsheadsmasterem
374
- # Ref disambiguation rules
375
- #
376
- # @example
377
- # lib.rev_parse('HEAD') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d'
378
- # lib.rev_parse('9b9b31e') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d'
379
- #
380
- # @param revision [String] the revision to resolve
381
- #
382
- # @return [String] the full commit hash
383
- #
384
- # @raise [Git::FailedError] if the revision cannot be resolved
385
- # @raise [ArgumentError] if the revision is a string starting with a hyphen
386
- #
387
- def rev_parse(revision)
388
- assert_args_are_not_options('rev', revision)
389
-
390
- command('rev-parse', '--revs-only', '--end-of-options', revision, '--')
391
- end
392
-
393
- # For backwards compatibility with the old method name
394
- alias revparse rev_parse
395
-
396
- # Find the first symbolic name for given commit_ish
397
- #
398
- # @param commit_ish [String] the commit_ish to find the symbolic name of
399
- #
400
- # @return [String, nil] the first symbolic name or nil if the commit_ish isn't found
401
- #
402
- # @raise [ArgumentError] if the commit_ish is a string starting with a hyphen
403
- #
404
- def name_rev(commit_ish)
405
- assert_args_are_not_options('commit_ish', commit_ish)
406
-
407
- command('name-rev', commit_ish).split[1]
408
- end
409
-
410
- alias namerev name_rev
411
-
412
- # Output the contents or other properties of one or more objects.
413
- #
414
- # @see https://git-scm.com/docs/git-cat-file git-cat-file
415
- #
416
- # @example Get the contents of a file without a block
417
- # lib.cat_file_contents('README.md') # => "This is a README file\n"
418
- #
419
- # @example Get the contents of a file with a block
420
- # lib.cat_file_contents('README.md') { |f| f.read } # => "This is a README file\n"
421
- #
422
- # @param object [String] the object whose contents to return
423
- #
424
- # @return [String] the object contents
425
- #
426
- # @raise [ArgumentError] if object is a string starting with a hyphen
427
- #
428
- def cat_file_contents(object)
429
- assert_args_are_not_options('object', object)
430
-
431
- if block_given?
432
- Tempfile.create do |file|
433
- # If a block is given, write the output from the process to a temporary
434
- # file and then yield the file to the block
435
- #
436
- command('cat-file', '-p', object, out: file, err: file)
437
- file.rewind
438
- yield file
439
- end
440
- else
441
- # If a block is not given, return the file contents as a string
442
- command('cat-file', '-p', object)
443
- end
444
- end
445
-
446
- alias object_contents cat_file_contents
447
-
448
- # Get the type for the given object
449
- #
450
- # @see https://git-scm.com/docs/git-cat-file git-cat-file
451
- #
452
- # @param object [String] the object to get the type
453
- #
454
- # @return [String] the object type
455
- #
456
- # @raise [ArgumentError] if object is a string starting with a hyphen
457
- #
458
- def cat_file_type(object)
459
- assert_args_are_not_options('object', object)
460
-
461
- command('cat-file', '-t', object)
462
- end
463
-
464
- alias object_type cat_file_type
465
-
466
- # Get the size for the given object
467
- #
468
- # @see https://git-scm.com/docs/git-cat-file git-cat-file
469
- #
470
- # @param object [String] the object to get the type
471
- #
472
- # @return [String] the object type
473
- #
474
- # @raise [ArgumentError] if object is a string starting with a hyphen
475
- #
476
- def cat_file_size(object)
477
- assert_args_are_not_options('object', object)
478
-
479
- command('cat-file', '-s', object).to_i
480
- end
481
-
482
- alias object_size cat_file_size
483
-
484
- # Return a hash of commit data
485
- #
486
- # @see https://git-scm.com/docs/git-cat-file git-cat-file
487
- #
488
- # @param object [String] the object to get the type
489
- #
490
- # @return [Hash] commit data
491
- #
492
- # The returned commit data has the following keys:
493
- # * tree [String]
494
- # * parent [Array<String>]
495
- # * author [String] the author name, email, and commit timestamp
496
- # * committer [String] the committer name, email, and merge timestamp
497
- # * message [String] the commit message
498
- # * gpgsig [String] the public signing key of the commit (if signed)
499
- #
500
- # @raise [ArgumentError] if object is a string starting with a hyphen
501
- #
502
- def cat_file_commit(object)
503
- assert_args_are_not_options('object', object)
504
-
505
- cdata = command_lines('cat-file', 'commit', object)
506
- process_commit_data(cdata, object)
507
- end
508
-
509
- alias commit_data cat_file_commit
510
-
511
- def process_commit_data(data, sha)
512
- # process_commit_headers consumes the header lines from the `data` array,
513
- # leaving only the message lines behind.
514
- headers = process_commit_headers(data)
515
- message = "#{data.join("\n")}\n"
516
-
517
- { 'sha' => sha, 'message' => message }.merge(headers)
518
- end
519
-
520
- CAT_FILE_HEADER_LINE = /\A(?<key>\w+) (?<value>.*)\z/
521
-
522
- def each_cat_file_header(data)
523
- while (match = CAT_FILE_HEADER_LINE.match(data.shift))
524
- key = match[:key]
525
- value_lines = [match[:value]]
526
-
527
- value_lines << data.shift.lstrip while data.first.start_with?(' ')
528
-
529
- yield key, value_lines.join("\n")
530
- end
531
- end
532
-
533
- # Return a hash of annotated tag data
534
- #
535
- # Does not work with lightweight tags. List all annotated tags in your repository
536
- # with the following command:
537
- #
538
- # ```sh
539
- # git for-each-ref --format='%(refname:strip=2)' refs/tags | \
540
- # while read tag; do git cat-file tag $tag >/dev/null 2>&1 && echo $tag; done
541
- # ```
542
- #
543
- # @see https://git-scm.com/docs/git-cat-file git-cat-file
544
- #
545
- # @param object [String] the tag to retrieve
546
- #
547
- # @return [Hash] tag data
548
- #
549
- # Example tag data returned:
550
- # ```ruby
551
- # {
552
- # "name" => "annotated_tag",
553
- # "object" => "46abbf07e3c564c723c7c039a43ab3a39e5d02dd",
554
- # "type" => "commit",
555
- # "tag" => "annotated_tag",
556
- # "tagger" => "Scott Chacon <schacon@gmail.com> 1724799270 -0700",
557
- # "message" => "Creating an annotated tag\n"
558
- # }
559
- # ```
560
- #
561
- # The returned commit data has the following keys:
562
- # * object [String] the sha of the tag object
563
- # * type [String]
564
- # * tag [String] tag name
565
- # * tagger [String] the name and email of the user who created the tag
566
- # and the timestamp of when the tag was created
567
- # * message [String] the tag message
568
- #
569
- # @raise [ArgumentError] if object is a string starting with a hyphen
570
- #
571
- def cat_file_tag(object)
572
- assert_args_are_not_options('object', object)
573
-
574
- tdata = command_lines('cat-file', 'tag', object)
575
- process_tag_data(tdata, object)
576
- end
577
-
578
- alias tag_data cat_file_tag
579
-
580
- def process_tag_data(data, name)
581
- hsh = { 'name' => name }
582
-
583
- each_cat_file_header(data) do |key, value|
584
- hsh[key] = value
585
- end
586
-
587
- hsh['message'] = "#{data.join("\n")}\n"
588
-
589
- hsh
590
- end
591
-
592
- def process_commit_log_data(data)
593
- RawLogParser.new(data).parse
594
- end
595
-
596
- # A private parser class to process the output of `git log --pretty=raw`
597
- # @api private
598
- class RawLogParser
599
- def initialize(lines)
600
- @lines = lines
601
- @commits = []
602
- @current_commit = nil
603
- @in_message = false
604
- end
605
-
606
- def parse
607
- @lines.each { |line| process_line(line.chomp) }
608
- finalize_commit
609
- @commits
610
- end
611
-
612
- private
613
-
614
- def process_line(line)
615
- if line.empty?
616
- @in_message = !@in_message
617
- return
618
- end
619
-
620
- @in_message = false if @in_message && !line.start_with?(' ')
621
-
622
- @in_message ? process_message_line(line) : process_metadata_line(line)
623
- end
624
-
625
- def process_message_line(line)
626
- @current_commit['message'] << "#{line[4..]}\n"
627
- end
628
-
629
- def process_metadata_line(line)
630
- key, *value = line.split
631
- value = value.join(' ')
632
-
633
- case key
634
- when 'commit'
635
- start_new_commit(value)
636
- when 'parent'
637
- @current_commit['parent'] << value
638
- else
639
- @current_commit[key] = value
640
- end
641
- end
642
-
643
- def start_new_commit(sha)
644
- finalize_commit
645
- @current_commit = { 'sha' => sha, 'message' => +'', 'parent' => [] }
646
- end
647
-
648
- def finalize_commit
649
- @commits << @current_commit if @current_commit
650
- end
651
- end
652
- private_constant :RawLogParser
653
-
654
- LS_TREE_OPTION_MAP = [
655
- { keys: [:recursive], flag: '-r', type: :boolean }
656
- ].freeze
657
-
658
- def ls_tree(sha, opts = {})
659
- data = { 'blob' => {}, 'tree' => {}, 'commit' => {} }
660
- args = build_args(opts, LS_TREE_OPTION_MAP)
661
-
662
- args.unshift(sha)
663
- args << opts[:path] if opts[:path]
664
-
665
- command_lines('ls-tree', *args).each do |line|
666
- (info, filenm) = split_status_line(line)
667
- (mode, type, sha) = info.split
668
- data[type][filenm] = { mode: mode, sha: sha }
669
- end
670
-
671
- data
672
- end
673
-
674
- def mv(file1, file2)
675
- command_lines('mv', '--', file1, file2)
676
- end
677
-
678
- def full_tree(sha)
679
- command_lines('ls-tree', '-r', sha)
680
- end
681
-
682
- def tree_depth(sha)
683
- full_tree(sha).size
684
- end
685
-
686
- def change_head_branch(branch_name)
687
- command('symbolic-ref', 'HEAD', "refs/heads/#{branch_name}")
688
- end
689
-
690
- BRANCH_LINE_REGEXP = /
691
- ^
692
- # Prefix indicates if this branch is checked out. The prefix is one of:
693
- (?:
694
- (?<current>\*[[:blank:]]) | # Current branch (checked out in the current worktree)
695
- (?<worktree>\+[[:blank:]]) | # Branch checked out in a different worktree
696
- [[:blank:]]{2} # Branch not checked out
697
- )
698
-
699
- # The branch's full refname
700
- (?:
701
- (?<not_a_branch>\(not[[:blank:]]a[[:blank:]]branch\)) |
702
- (?:\(HEAD[[:blank:]]detached[[:blank:]]at[[:blank:]](?<detached_ref>[^)]+)\)) |
703
- (?<refname>[^[[:blank:]]]+)
704
- )
705
-
706
- # Optional symref
707
- # If this ref is a symbolic reference, this is the ref referenced
708
- (?:
709
- [[:blank:]]->[[:blank:]](?<symref>.*)
710
- )?
711
- $
712
- /x
713
-
714
- def branches_all
715
- lines = command_lines('branch', '-a')
716
- lines.each_with_index.filter_map do |line, index|
717
- parse_branch_line(line, index, lines)
718
- end
719
- end
720
-
721
- def worktrees_all
722
- arr = []
723
- directory = ''
724
- # Output example for `worktree list --porcelain`:
725
- # worktree /code/public/ruby-git
726
- # HEAD 4bef5abbba073c77b4d0ccc1ffcd0ed7d48be5d4
727
- # branch refs/heads/master
728
- #
729
- # worktree /tmp/worktree-1
730
- # HEAD b8c63206f8d10f57892060375a86ae911fad356e
731
- # detached
732
- #
733
- command_lines('worktree', 'list', '--porcelain').each do |w|
734
- s = w.split
735
- directory = s[1] if s[0] == 'worktree'
736
- arr << [directory, s[1]] if s[0] == 'HEAD'
737
- end
738
- arr
739
- end
740
-
741
- def worktree_add(dir, commitish = nil)
742
- return worktree_command('worktree', 'add', dir, commitish) unless commitish.nil?
743
-
744
- worktree_command('worktree', 'add', dir)
745
- end
746
-
747
- def worktree_remove(dir)
748
- worktree_command('worktree', 'remove', dir)
749
- end
750
-
751
- def worktree_prune
752
- worktree_command('worktree', 'prune')
753
- end
754
-
755
- def list_files(ref_dir)
756
- dir = File.join(@git_dir, 'refs', ref_dir)
757
- Dir.glob('**/*', base: dir).select { |f| File.file?(File.join(dir, f)) }
758
- end
759
-
760
- # The state and name of branch pointed to by `HEAD`
761
- #
762
- # HEAD can be in the following states:
763
- #
764
- # **:active**: `HEAD` points to a branch reference which in turn points to a
765
- # commit representing the tip of that branch. This is the typical state when
766
- # working on a branch.
767
- #
768
- # **:unborn**: `HEAD` points to a branch reference that does not yet exist
769
- # because no commits have been made on that branch. This state occurs in two
770
- # scenarios:
771
- #
772
- # * When a repository is newly initialized, and no commits have been made on the
773
- # initial branch.
774
- # * When a new branch is created using `git checkout --orphan <branch>`, starting
775
- # a new branch with no history.
776
- #
777
- # **:detached**: `HEAD` points directly to a specific commit (identified by its
778
- # SHA) rather than a branch reference. This state occurs when you check out a
779
- # commit, a tag, or any state that is not directly associated with a branch. The
780
- # branch name in this case is `HEAD`.
781
- #
782
- HeadState = Struct.new(:state, :name)
783
-
784
- # The current branch state which is the state of `HEAD`
785
- #
786
- # @return [HeadState] the state and name of the current branch
787
- #
788
- def current_branch_state
789
- branch_name = command('branch', '--show-current')
790
- return HeadState.new(:detached, 'HEAD') if branch_name.empty?
791
-
792
- state = get_branch_state(branch_name)
793
- HeadState.new(state, branch_name)
794
- end
795
-
796
- def branch_current
797
- branch_name = command('branch', '--show-current')
798
- branch_name.empty? ? 'HEAD' : branch_name
799
- end
800
-
801
- def branch_contains(commit, branch_name = '')
802
- command('branch', branch_name, '--contains', commit)
803
- end
804
-
805
- GREP_OPTION_MAP = [
806
- { keys: [:ignore_case], flag: '-i', type: :boolean },
807
- { keys: [:invert_match], flag: '-v', type: :boolean },
808
- { keys: [:extended_regexp], flag: '-E', type: :boolean },
809
- # For validation only, as these are handled manually
810
- { keys: [:object], type: :validate_only },
811
- { keys: [:path_limiter], type: :validate_only }
812
- ].freeze
813
-
814
- # returns hash
815
- # [tree-ish] = [[line_no, match], [line_no, match2]]
816
- # [tree-ish] = [[line_no, match], [line_no, match2]]
817
- def grep(string, opts = {})
818
- opts[:object] ||= 'HEAD'
819
- ArgsBuilder.validate!(opts, GREP_OPTION_MAP)
820
-
821
- boolean_flags = build_args(opts, GREP_OPTION_MAP)
822
- args = ['-n', *boolean_flags, '-e', string, opts[:object]]
823
-
824
- if (limiter = opts[:path_limiter])
825
- args.push('--', *Array(limiter))
826
- end
827
-
828
- lines = execute_grep_command(args)
829
- parse_grep_output(lines)
830
- end
831
-
832
- # Validate that the given arguments cannot be mistaken for a command-line option
833
- #
834
- # @param arg_name [String] the name of the arguments to mention in the error message
835
- # @param args [Array<String, nil>] the arguments to validate
836
- #
837
- # @raise [ArgumentError] if any of the parameters are a string starting with a hyphen
838
- # @return [void]
839
- #
840
- def assert_args_are_not_options(arg_name, *args)
841
- invalid_args = args.select { |arg| arg&.start_with?('-') }
842
- return unless invalid_args.any?
843
-
844
- raise ArgumentError, "Invalid #{arg_name}: '#{invalid_args.join("', '")}'"
845
- end
846
-
847
- # Normalizes path specifications for Git commands
848
- #
849
- # Converts a single path or array of paths into a consistent array format
850
- # suitable for appending to Git command arguments after '--'. Empty strings
851
- # are filtered out after conversion.
852
- #
853
- # @param pathspecs [String, Pathname, Array<String, Pathname>, nil] path(s) to normalize
854
- # @param arg_name [String] name of the argument for error messages
855
- # @return [Array<String>, nil] normalized array of path strings, or nil if empty/nil input
856
- # @raise [ArgumentError] if any path is not a String or Pathname
857
- #
858
- def normalize_pathspecs(pathspecs, arg_name)
859
- return nil unless pathspecs
860
-
861
- normalized = Array(pathspecs)
862
- validate_pathspec_types(normalized, arg_name)
863
-
864
- normalized = normalized.map(&:to_s).reject(&:empty?)
865
- return nil if normalized.empty?
866
-
867
- normalized
868
- end
869
-
870
- # Validates that all pathspecs are String or Pathname objects
871
- #
872
- # @param pathspecs [Array] the pathspecs to validate
873
- # @param arg_name [String] name of the argument for error messages
874
- # @raise [ArgumentError] if any path is not a String or Pathname
875
- #
876
- def validate_pathspec_types(pathspecs, arg_name)
877
- return if pathspecs.all? { |path| path.is_a?(String) || path.is_a?(Pathname) }
878
-
879
- raise ArgumentError, "Invalid #{arg_name}: must be a String, Pathname, or Array of Strings/Pathnames"
880
- end
881
-
882
- # Handle deprecated :path option in favor of :path_limiter
883
- def handle_deprecated_path_option(opts)
884
- if opts.key?(:path_limiter)
885
- opts[:path_limiter]
886
- elsif opts.key?(:path)
887
- Git::Deprecation.warn(
888
- 'Git::Lib#diff_path_status :path option is deprecated. Use :path_limiter instead.'
889
- )
890
- opts[:path]
891
- end
892
- end
893
-
894
- DIFF_FULL_OPTION_MAP = [
895
- { type: :static, flag: '-p' },
896
- { keys: [:path_limiter], type: :validate_only }
897
- ].freeze
898
-
899
- def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
900
- assert_args_are_not_options('commit or commit range', obj1, obj2)
901
- ArgsBuilder.validate!(opts, DIFF_FULL_OPTION_MAP)
902
-
903
- args = build_args(opts, DIFF_FULL_OPTION_MAP)
904
- args.push(obj1, obj2).compact!
905
-
906
- if (pathspecs = normalize_pathspecs(opts[:path_limiter], 'path limiter'))
907
- args.push('--', *pathspecs)
908
- end
909
-
910
- command('diff', *args)
911
- end
912
-
913
- DIFF_STATS_OPTION_MAP = [
914
- { type: :static, flag: '--numstat' },
915
- { keys: [:path_limiter], type: :validate_only }
916
- ].freeze
917
-
918
- def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
919
- assert_args_are_not_options('commit or commit range', obj1, obj2)
920
- ArgsBuilder.validate!(opts, DIFF_STATS_OPTION_MAP)
921
-
922
- args = build_args(opts, DIFF_STATS_OPTION_MAP)
923
- args.push(obj1, obj2).compact!
924
-
925
- if (pathspecs = normalize_pathspecs(opts[:path_limiter], 'path limiter'))
926
- args.push('--', *pathspecs)
927
- end
928
-
929
- output_lines = command_lines('diff', *args)
930
- parse_diff_stats_output(output_lines)
931
- end
932
-
933
- DIFF_PATH_STATUS_OPTION_MAP = [
934
- { type: :static, flag: '--name-status' },
935
- { keys: [:path_limiter], type: :validate_only },
936
- { keys: [:path], type: :validate_only }
937
- ].freeze
938
-
939
- def diff_path_status(reference1 = nil, reference2 = nil, opts = {})
940
- assert_args_are_not_options('commit or commit range', reference1, reference2)
941
- ArgsBuilder.validate!(opts, DIFF_PATH_STATUS_OPTION_MAP)
942
-
943
- args = build_args(opts, DIFF_PATH_STATUS_OPTION_MAP)
944
- args.push(reference1, reference2).compact!
945
-
946
- path_limiter = handle_deprecated_path_option(opts)
947
- if (pathspecs = normalize_pathspecs(path_limiter, 'path limiter'))
948
- args.push('--', *pathspecs)
949
- end
950
-
951
- parse_diff_path_status(args)
952
- end
953
-
954
- # compares the index and the working directory
955
- def diff_files
956
- diff_as_hash('diff-files')
957
- end
958
-
959
- # compares the index and the repository
960
- def diff_index(treeish)
961
- diff_as_hash('diff-index', treeish)
962
- end
963
-
964
- # List all files that are in the index
965
- #
966
- # @param location [String] the location to list the files from
967
- #
968
- # @return [Hash<String, Hash>] a hash of files in the index
969
- # * key: file [String] the file path
970
- # * value: file_info [Hash] the file information containing the following keys:
971
- # * :path [String] the file path
972
- # * :mode_index [String] the file mode
973
- # * :sha_index [String] the file sha
974
- # * :stage [String] the file stage
975
- #
976
- def ls_files(location = nil)
977
- location ||= '.'
978
- {}.tap do |files|
979
- command_lines('ls-files', '--stage', location).each do |line|
980
- (info, file) = split_status_line(line)
981
- (mode, sha, stage) = info.split
982
- files[file] = {
983
- path: file, mode_index: mode, sha_index: sha, stage: stage
984
- }
985
- end
986
- end
987
- end
988
-
989
- # Unescape a path if it is quoted
990
- #
991
- # Git commands that output paths (e.g. ls-files, diff), will escape unusual
992
- # characters.
993
- #
994
- # @example
995
- # lib.unescape_if_quoted('"quoted_file_\\342\\230\\240"') # => 'quoted_file_☠'
996
- # lib.unescape_if_quoted('unquoted_file') # => 'unquoted_file'
997
- #
998
- # @param path [String] the path to unescape if quoted
999
- #
1000
- # @return [String] the unescaped path if quoted otherwise the original path
1001
- #
1002
- # @api private
1003
- #
1004
- def unescape_quoted_path(path)
1005
- if path.start_with?('"') && path.end_with?('"')
1006
- Git::EscapedPath.new(path[1..-2]).unescape
1007
- else
1008
- path
1009
- end
1010
- end
1011
-
1012
- LS_REMOTE_OPTION_MAP = [
1013
- { keys: [:refs], flag: '--refs', type: :boolean }
1014
- ].freeze
1015
-
1016
- def ls_remote(location = nil, opts = {})
1017
- ArgsBuilder.validate!(opts, LS_REMOTE_OPTION_MAP)
1018
-
1019
- flags = build_args(opts, LS_REMOTE_OPTION_MAP)
1020
- positional_arg = location || '.'
1021
-
1022
- output_lines = command_lines('ls-remote', *flags, positional_arg)
1023
- parse_ls_remote_output(output_lines)
1024
- end
1025
-
1026
- def ignored_files
1027
- command_lines('ls-files', '--others', '-i', '--exclude-standard').map { |f| unescape_quoted_path(f) }
1028
- end
1029
-
1030
- def untracked_files
1031
- command_lines('ls-files', '--others', '--exclude-standard', chdir: @git_work_dir).map do |f|
1032
- unescape_quoted_path(f)
1033
- end
1034
- end
1035
-
1036
- def config_remote(name)
1037
- hsh = {}
1038
- config_list.each do |key, value|
1039
- hsh[key.gsub("remote.#{name}.", '')] = value if /remote.#{name}/.match(key)
1040
- end
1041
- hsh
1042
- end
1043
-
1044
- def config_get(name)
1045
- command('config', '--get', name, chdir: @git_dir)
1046
- end
1047
-
1048
- def global_config_get(name)
1049
- command('config', '--global', '--get', name)
1050
- end
1051
-
1052
- def config_list
1053
- parse_config_list command_lines('config', '--list', chdir: @git_dir)
1054
- end
1055
-
1056
- def global_config_list
1057
- parse_config_list command_lines('config', '--global', '--list')
1058
- end
1059
-
1060
- def parse_config_list(lines)
1061
- hsh = {}
1062
- lines.each do |line|
1063
- (key, *values) = line.split('=')
1064
- hsh[key] = values.join('=')
1065
- end
1066
- hsh
1067
- end
1068
-
1069
- def parse_config(file)
1070
- parse_config_list command_lines('config', '--list', '--file', file)
1071
- end
1072
-
1073
- # Shows objects
1074
- #
1075
- # @param [String|NilClass] objectish the target object reference (nil == HEAD)
1076
- # @param [String|NilClass] path the path of the file to be shown
1077
- # @return [String] the object information
1078
- def show(objectish = nil, path = nil)
1079
- arr_opts = []
1080
-
1081
- arr_opts << (path ? "#{objectish}:#{path}" : objectish)
1082
-
1083
- command('show', *arr_opts.compact, chomp: false)
1084
- end
1085
-
1086
- ## WRITE COMMANDS ##
1087
-
1088
- CONFIG_SET_OPTION_MAP = [
1089
- { keys: [:file], flag: '--file', type: :valued_space }
1090
- ].freeze
1091
-
1092
- def config_set(name, value, options = {})
1093
- ArgsBuilder.validate!(options, CONFIG_SET_OPTION_MAP)
1094
- flags = build_args(options, CONFIG_SET_OPTION_MAP)
1095
- command('config', *flags, name, value)
1096
- end
1097
-
1098
- def global_config_set(name, value)
1099
- command('config', '--global', name, value)
1100
- end
1101
-
1102
- ADD_OPTION_MAP = [
1103
- { keys: [:all], flag: '--all', type: :boolean },
1104
- { keys: [:force], flag: '--force', type: :boolean }
1105
- ].freeze
1106
-
1107
- # Update the index from the current worktree to prepare the for the next commit
1108
- #
1109
- # @example
1110
- # lib.add('path/to/file')
1111
- # lib.add(['path/to/file1','path/to/file2'])
1112
- # lib.add(:all => true)
1113
- #
1114
- # @param [String, Array<String>] paths files to be added to the repository (relative to the worktree root)
1115
- # @param [Hash] options
1116
- #
1117
- # @option options [Boolean] :all Add, modify, and remove index entries to match the worktree
1118
- # @option options [Boolean] :force Allow adding otherwise ignored files
1119
- #
1120
- def add(paths = '.', options = {})
1121
- args = build_args(options, ADD_OPTION_MAP)
1122
-
1123
- args << '--'
1124
- args.concat(Array(paths))
1125
-
1126
- command('add', *args)
1127
- end
1128
-
1129
- RM_OPTION_MAP = [
1130
- { type: :static, flag: '-f' },
1131
- { keys: [:recursive], flag: '-r', type: :boolean },
1132
- { keys: [:cached], flag: '--cached', type: :boolean }
1133
- ].freeze
1134
-
1135
- def rm(path = '.', opts = {})
1136
- args = build_args(opts, RM_OPTION_MAP)
1137
-
1138
- args << '--'
1139
- args.concat(Array(path))
1140
-
1141
- command('rm', *args)
1142
- end
1143
-
1144
- # Returns true if the repository is empty (meaning it has no commits)
1145
- #
1146
- # @return [Boolean]
1147
- #
1148
- def empty?
1149
- command('rev-parse', '--verify', 'HEAD')
1150
- false
1151
- rescue Git::FailedError => e
1152
- raise unless e.result.status.exitstatus == 128 &&
1153
- e.result.stderr == 'fatal: Needed a single revision'
1154
-
1155
- true
1156
- end
1157
-
1158
- COMMIT_OPTION_MAP = [
1159
- { keys: %i[add_all all], flag: '--all', type: :boolean },
1160
- { keys: [:allow_empty], flag: '--allow-empty', type: :boolean },
1161
- { keys: [:no_verify], flag: '--no-verify', type: :boolean },
1162
- { keys: [:allow_empty_message], flag: '--allow-empty-message', type: :boolean },
1163
- { keys: [:author], flag: '--author', type: :valued_equals },
1164
- { keys: [:message], flag: '--message', type: :valued_equals },
1165
- { keys: [:no_gpg_sign], flag: '--no-gpg-sign', type: :boolean },
1166
- { keys: [:date], flag: '--date', type: :valued_equals, validator: ->(v) { v.is_a?(String) } },
1167
- { keys: [:amend], type: :custom, builder: ->(value) { ['--amend', '--no-edit'] if value } },
1168
- {
1169
- keys: [:gpg_sign],
1170
- type: :custom,
1171
- builder: lambda { |value|
1172
- if value
1173
- value == true ? '--gpg-sign' : "--gpg-sign=#{value}"
1174
- end
1175
- }
1176
- }
1177
- ].freeze
1178
-
1179
- # Takes the commit message with the options and executes the commit command
1180
- #
1181
- # accepts options:
1182
- # :amend
1183
- # :all
1184
- # :allow_empty
1185
- # :author
1186
- # :date
1187
- # :no_verify
1188
- # :allow_empty_message
1189
- # :gpg_sign (accepts true or a gpg key ID as a String)
1190
- # :no_gpg_sign (conflicts with :gpg_sign)
1191
- #
1192
- # @param [String] message the commit message to be used
1193
- # @param [Hash] opts the commit options to be used
1194
-
1195
- def commit(message, opts = {})
1196
- opts[:message] = message if message # Handle message arg for backward compatibility
1197
-
1198
- # Perform cross-option validation before building args
1199
- raise ArgumentError, 'cannot specify :gpg_sign and :no_gpg_sign' if opts[:gpg_sign] && opts[:no_gpg_sign]
1200
-
1201
- ArgsBuilder.validate!(opts, COMMIT_OPTION_MAP)
1202
-
1203
- args = build_args(opts, COMMIT_OPTION_MAP)
1204
- command('commit', *args)
1205
- end
1206
- RESET_OPTION_MAP = [
1207
- { keys: [:hard], flag: '--hard', type: :boolean }
1208
- ].freeze
1209
-
1210
- def reset(commit, opts = {})
1211
- args = build_args(opts, RESET_OPTION_MAP)
1212
- args << commit if commit
1213
- command('reset', *args)
1214
- end
1215
-
1216
- CLEAN_OPTION_MAP = [
1217
- { keys: [:force], flag: '--force', type: :boolean },
1218
- { keys: [:ff], flag: '-ff', type: :boolean },
1219
- { keys: [:d], flag: '-d', type: :boolean },
1220
- { keys: [:x], flag: '-x', type: :boolean }
1221
- ].freeze
1222
-
1223
- def clean(opts = {})
1224
- args = build_args(opts, CLEAN_OPTION_MAP)
1225
- command('clean', *args)
1226
- end
1227
-
1228
- REVERT_OPTION_MAP = [
1229
- { keys: [:no_edit], flag: '--no-edit', type: :boolean }
1230
- ].freeze
1231
-
1232
- def revert(commitish, opts = {})
1233
- # Forcing --no-edit as default since it's not an interactive session.
1234
- opts = { no_edit: true }.merge(opts)
1235
-
1236
- args = build_args(opts, REVERT_OPTION_MAP)
1237
- args << commitish
1238
-
1239
- command('revert', *args)
1240
- end
1241
-
1242
- def apply(patch_file)
1243
- arr_opts = []
1244
- arr_opts << '--' << patch_file if patch_file
1245
- command('apply', *arr_opts)
1246
- end
1247
-
1248
- def apply_mail(patch_file)
1249
- arr_opts = []
1250
- arr_opts << '--' << patch_file if patch_file
1251
- command('am', *arr_opts)
1252
- end
1253
-
1254
- def stashes_all
1255
- stash_log_lines.each_with_index.map do |line, index|
1256
- parse_stash_log_line(line, index)
1257
- end
1258
- end
1259
-
1260
- def stash_save(message)
1261
- output = command('stash', 'save', message)
1262
- output =~ /HEAD is now at/
1263
- end
1264
-
1265
- def stash_apply(id = nil)
1266
- if id
1267
- command('stash', 'apply', id)
1268
- else
1269
- command('stash', 'apply')
1270
- end
1271
- end
1272
-
1273
- def stash_clear
1274
- command('stash', 'clear')
1275
- end
1276
-
1277
- def stash_list
1278
- command('stash', 'list')
1279
- end
1280
-
1281
- def branch_new(branch)
1282
- command('branch', branch)
1283
- end
1284
-
1285
- def branch_delete(branch)
1286
- command('branch', '-D', branch)
1287
- end
1288
-
1289
- CHECKOUT_OPTION_MAP = [
1290
- { keys: %i[force f], flag: '--force', type: :boolean },
1291
- { keys: %i[new_branch b], type: :validate_only },
1292
- { keys: [:start_point], type: :validate_only }
1293
- ].freeze
1294
-
1295
- # Runs checkout command to checkout or create branch
1296
- #
1297
- # accepts options:
1298
- # :new_branch
1299
- # :force
1300
- # :start_point
1301
- #
1302
- # @param [String] branch
1303
- # @param [Hash] opts
1304
- def checkout(branch = nil, opts = {})
1305
- if branch.is_a?(Hash) && opts.empty?
1306
- opts = branch
1307
- branch = nil
1308
- end
1309
- ArgsBuilder.validate!(opts, CHECKOUT_OPTION_MAP)
1310
-
1311
- flags = build_args(opts, CHECKOUT_OPTION_MAP)
1312
- positional_args = build_checkout_positional_args(branch, opts)
1313
-
1314
- command('checkout', *flags, *positional_args)
1315
- end
1316
-
1317
- def checkout_file(version, file)
1318
- arr_opts = []
1319
- arr_opts << version
1320
- arr_opts << file
1321
- command('checkout', *arr_opts)
1322
- end
1323
-
1324
- MERGE_OPTION_MAP = [
1325
- { keys: [:no_commit], flag: '--no-commit', type: :boolean },
1326
- { keys: [:no_ff], flag: '--no-ff', type: :boolean },
1327
- { keys: [:m], flag: '-m', type: :valued_space }
1328
- ].freeze
1329
-
1330
- def merge(branch, message = nil, opts = {})
1331
- # For backward compatibility, treat the message arg as the :m option.
1332
- opts[:m] = message if message
1333
- ArgsBuilder.validate!(opts, MERGE_OPTION_MAP)
1334
-
1335
- args = build_args(opts, MERGE_OPTION_MAP)
1336
- args.concat(Array(branch))
1337
-
1338
- command('merge', *args)
1339
- end
1340
-
1341
- MERGE_BASE_OPTION_MAP = [
1342
- { keys: [:octopus], flag: '--octopus', type: :boolean },
1343
- { keys: [:independent], flag: '--independent', type: :boolean },
1344
- { keys: [:fork_point], flag: '--fork-point', type: :boolean },
1345
- { keys: [:all], flag: '--all', type: :boolean }
1346
- ].freeze
1347
-
1348
- def merge_base(*args)
1349
- opts = args.last.is_a?(Hash) ? args.pop : {}
1350
- ArgsBuilder.validate!(opts, MERGE_BASE_OPTION_MAP)
1351
-
1352
- flags = build_args(opts, MERGE_BASE_OPTION_MAP)
1353
- command_args = flags + args
1354
-
1355
- command('merge-base', *command_args).lines.map(&:strip)
1356
- end
1357
-
1358
- def unmerged
1359
- unmerged = []
1360
- command_lines('diff', '--cached').each do |line|
1361
- unmerged << ::Regexp.last_match(1) if line =~ /^\* Unmerged path (.*)/
1362
- end
1363
- unmerged
1364
- end
1365
-
1366
- def conflicts # :yields: file, your, their
1367
- unmerged.each do |file_path|
1368
- Tempfile.create(['YOUR-', File.basename(file_path)]) do |your_file|
1369
- write_staged_content(file_path, 2, your_file).flush
1370
-
1371
- Tempfile.create(['THEIR-', File.basename(file_path)]) do |their_file|
1372
- write_staged_content(file_path, 3, their_file).flush
1373
- yield(file_path, your_file.path, their_file.path)
1374
- end
1375
- end
1376
- end
1377
- end
1378
-
1379
- REMOTE_ADD_OPTION_MAP = [
1380
- { keys: %i[with_fetch fetch], flag: '-f', type: :boolean },
1381
- { keys: [:track], flag: '-t', type: :valued_space }
1382
- ].freeze
1383
-
1384
- def remote_add(name, url, opts = {})
1385
- ArgsBuilder.validate!(opts, REMOTE_ADD_OPTION_MAP)
1386
-
1387
- flags = build_args(opts, REMOTE_ADD_OPTION_MAP)
1388
- positional_args = ['--', name, url]
1389
- command_args = ['add'] + flags + positional_args
1390
-
1391
- command('remote', *command_args)
1392
- end
1393
-
1394
- REMOTE_SET_BRANCHES_OPTION_MAP = [
1395
- { keys: [:add], flag: '--add', type: :boolean }
1396
- ].freeze
1397
-
1398
- def remote_set_branches(name, branches, opts = {})
1399
- ArgsBuilder.validate!(opts, REMOTE_SET_BRANCHES_OPTION_MAP)
1400
-
1401
- flags = build_args(opts, REMOTE_SET_BRANCHES_OPTION_MAP)
1402
- branch_args = Array(branches).flatten
1403
- command_args = ['set-branches'] + flags + [name] + branch_args
1404
-
1405
- command('remote', *command_args)
1406
- end
1407
-
1408
- def remote_set_url(name, url)
1409
- arr_opts = ['set-url']
1410
- arr_opts << name
1411
- arr_opts << url
1412
-
1413
- command('remote', *arr_opts)
1414
- end
1415
-
1416
- def remote_remove(name)
1417
- command('remote', 'rm', name)
1418
- end
1419
-
1420
- def remotes
1421
- command_lines('remote')
1422
- end
1423
-
1424
- def tags
1425
- command_lines('tag')
1426
- end
1427
-
1428
- TAG_OPTION_MAP = [
1429
- { keys: %i[force f], flag: '-f', type: :boolean },
1430
- { keys: %i[annotate a], flag: '-a', type: :boolean },
1431
- { keys: %i[sign s], flag: '-s', type: :boolean },
1432
- { keys: %i[delete d], flag: '-d', type: :boolean },
1433
- { keys: %i[message m], flag: '-m', type: :valued_space }
1434
- ].freeze
1435
-
1436
- def tag(name, *args)
1437
- opts = args.last.is_a?(Hash) ? args.pop : {}
1438
- target = args.first
1439
-
1440
- validate_tag_options!(opts)
1441
- ArgsBuilder.validate!(opts, TAG_OPTION_MAP)
1442
-
1443
- flags = build_args(opts, TAG_OPTION_MAP)
1444
- positional_args = [name, target].compact
1445
-
1446
- command('tag', *flags, *positional_args)
1447
- end
1448
-
1449
- FETCH_OPTION_MAP = [
1450
- { keys: [:all], flag: '--all', type: :boolean },
1451
- { keys: %i[tags t], flag: '--tags', type: :boolean },
1452
- { keys: %i[prune p], flag: '--prune', type: :boolean },
1453
- { keys: %i[prune-tags P], flag: '--prune-tags', type: :boolean },
1454
- { keys: %i[force f], flag: '--force', type: :boolean },
1455
- { keys: %i[update-head-ok u], flag: '--update-head-ok', type: :boolean },
1456
- { keys: [:unshallow], flag: '--unshallow', type: :boolean },
1457
- { keys: [:depth], flag: '--depth', type: :valued_space },
1458
- { keys: [:ref], type: :validate_only }
1459
- ].freeze
1460
-
1461
- def fetch(remote, opts)
1462
- ArgsBuilder.validate!(opts, FETCH_OPTION_MAP)
1463
- args = build_args(opts, FETCH_OPTION_MAP)
1464
-
1465
- if remote || opts[:ref]
1466
- args << '--'
1467
- args << remote if remote
1468
- args << opts[:ref] if opts[:ref]
1469
- end
1470
-
1471
- command('fetch', *args, merge: true)
1472
- end
1473
-
1474
- PUSH_OPTION_MAP = [
1475
- { keys: [:mirror], flag: '--mirror', type: :boolean },
1476
- { keys: [:delete], flag: '--delete', type: :boolean },
1477
- { keys: %i[force f], flag: '--force', type: :boolean },
1478
- { keys: [:push_option], flag: '--push-option', type: :repeatable_valued_space },
1479
- { keys: [:all], type: :validate_only }, # For validation purposes
1480
- { keys: [:tags], type: :validate_only } # From the `push` method's logic
1481
- ].freeze
1482
-
1483
- def push(remote = nil, branch = nil, opts = nil)
1484
- remote, branch, opts = normalize_push_args(remote, branch, opts)
1485
- ArgsBuilder.validate!(opts, PUSH_OPTION_MAP)
1486
-
1487
- raise ArgumentError, 'remote is required if branch is specified' if !remote && branch
1488
-
1489
- args = build_push_args(remote, branch, opts)
1490
-
1491
- if opts[:mirror]
1492
- command('push', *args)
1493
- else
1494
- command('push', *args)
1495
- command('push', '--tags', *(args - [branch].compact)) if opts[:tags]
1496
- end
1497
- end
1498
-
1499
- PULL_OPTION_MAP = [
1500
- { keys: [:allow_unrelated_histories], flag: '--allow-unrelated-histories', type: :boolean }
1501
- ].freeze
1502
-
1503
- def pull(remote = nil, branch = nil, opts = {})
1504
- raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?
1505
-
1506
- ArgsBuilder.validate!(opts, PULL_OPTION_MAP)
1507
-
1508
- flags = build_args(opts, PULL_OPTION_MAP)
1509
- positional_args = [remote, branch].compact
1510
-
1511
- command('pull', *flags, *positional_args)
1512
- end
1513
-
1514
- def tag_sha(tag_name)
1515
- head = File.join(@git_dir, 'refs', 'tags', tag_name)
1516
- return File.read(head).chomp if File.exist?(head)
1517
-
1518
- begin
1519
- command('show-ref', '--tags', '-s', tag_name)
1520
- rescue Git::FailedError => e
1521
- raise unless e.result.status.exitstatus == 1 && e.result.stderr == ''
1522
-
1523
- ''
1524
- end
1525
- end
1526
-
1527
- def repack
1528
- command('repack', '-a', '-d')
1529
- end
1530
-
1531
- def gc
1532
- command('gc', '--prune', '--aggressive', '--auto')
1533
- end
1534
-
1535
- FSCK_OPTION_MAP = [
1536
- { flag: '--no-progress', type: :static },
1537
- { keys: [:unreachable], flag: '--unreachable', type: :boolean },
1538
- { keys: [:strict], flag: '--strict', type: :boolean },
1539
- { keys: [:connectivity_only], flag: '--connectivity-only', type: :boolean },
1540
- { keys: [:root], flag: '--root', type: :boolean },
1541
- { keys: [:tags], flag: '--tags', type: :boolean },
1542
- { keys: [:cache], flag: '--cache', type: :boolean },
1543
- { keys: [:no_reflogs], flag: '--no-reflogs', type: :boolean },
1544
- { keys: [:lost_found], flag: '--lost-found', type: :boolean },
1545
- { keys: [:dangling], flag: '--dangling', type: :boolean_negatable },
1546
- { keys: [:full], flag: '--full', type: :boolean_negatable },
1547
- { keys: [:name_objects], flag: '--name-objects', type: :boolean_negatable },
1548
- { keys: [:references], flag: '--references', type: :boolean_negatable }
1549
- ].freeze
1550
-
1551
- def fsck(*objects, **opts)
1552
- args = ArgsBuilder.build(opts, FSCK_OPTION_MAP)
1553
- args.concat(objects) unless objects.empty?
1554
- # fsck returns non-zero exit status when issues are found:
1555
- # 1 = errors found, 2 = missing objects, 4 = warnings
1556
- # We still want to parse the output in these cases
1557
- output = begin
1558
- command('fsck', *args)
1559
- rescue Git::FailedError => e
1560
- raise unless [1, 2, 4].include?(e.result.status.exitstatus)
1561
-
1562
- e.result.stdout
1563
- end
1564
- parse_fsck_output(output)
1565
- end
1566
-
1567
- READ_TREE_OPTION_MAP = [
1568
- { keys: [:prefix], flag: '--prefix', type: :valued_equals }
1569
- ].freeze
1570
-
1571
- def read_tree(treeish, opts = {})
1572
- ArgsBuilder.validate!(opts, READ_TREE_OPTION_MAP)
1573
- flags = build_args(opts, READ_TREE_OPTION_MAP)
1574
- command('read-tree', *flags, treeish)
1575
- end
1576
-
1577
- def write_tree
1578
- command('write-tree')
1579
- end
1580
-
1581
- COMMIT_TREE_OPTION_MAP = [
1582
- { keys: %i[parent parents], flag: '-p', type: :repeatable_valued_space },
1583
- { keys: [:message], flag: '-m', type: :valued_space }
1584
- ].freeze
1585
-
1586
- def commit_tree(tree, opts = {})
1587
- opts[:message] ||= "commit tree #{tree}"
1588
- ArgsBuilder.validate!(opts, COMMIT_TREE_OPTION_MAP)
1589
-
1590
- flags = build_args(opts, COMMIT_TREE_OPTION_MAP)
1591
- command('commit-tree', tree, *flags)
1592
- end
1593
-
1594
- def update_ref(ref, commit)
1595
- command('update-ref', ref, commit)
1596
- end
1597
-
1598
- CHECKOUT_INDEX_OPTION_MAP = [
1599
- { keys: [:prefix], flag: '--prefix', type: :valued_equals },
1600
- { keys: [:force], flag: '--force', type: :boolean },
1601
- { keys: [:all], flag: '--all', type: :boolean },
1602
- { keys: [:path_limiter], type: :validate_only }
1603
- ].freeze
1604
-
1605
- def checkout_index(opts = {})
1606
- ArgsBuilder.validate!(opts, CHECKOUT_INDEX_OPTION_MAP)
1607
- args = build_args(opts, CHECKOUT_INDEX_OPTION_MAP)
1608
-
1609
- if (path = opts[:path_limiter]) && path.is_a?(String)
1610
- args.push('--', path)
1611
- end
1612
-
1613
- command('checkout-index', *args)
1614
- end
1615
-
1616
- ARCHIVE_OPTION_MAP = [
1617
- { keys: [:prefix], flag: '--prefix', type: :valued_equals },
1618
- { keys: [:remote], flag: '--remote', type: :valued_equals },
1619
- # These options are used by helpers or handled manually
1620
- { keys: [:path], type: :validate_only },
1621
- { keys: [:format], type: :validate_only },
1622
- { keys: [:add_gzip], type: :validate_only }
1623
- ].freeze
1624
-
1625
- def archive(sha, file = nil, opts = {})
1626
- ArgsBuilder.validate!(opts, ARCHIVE_OPTION_MAP)
1627
- file ||= temp_file_name
1628
- format, gzip = parse_archive_format_options(opts)
1629
-
1630
- args = build_args(opts, ARCHIVE_OPTION_MAP)
1631
- args.unshift("--format=#{format}")
1632
- args << sha
1633
- args.push('--', opts[:path]) if opts[:path]
1634
-
1635
- File.open(file, 'wb') { |f| command('archive', *args, out: f) }
1636
- apply_gzip(file) if gzip
1637
-
1638
- file
1639
- end
1640
-
1641
- # returns the current version of git, as an Array of Fixnums.
1642
- def current_command_version
1643
- output = command('version')
1644
- version = output[/\d+(\.\d+)+/]
1645
- version_parts = version.split('.').collect(&:to_i)
1646
- version_parts.fill(0, version_parts.length...3)
1647
- end
1648
-
1649
- # Returns current_command_version <=> other_version
1650
- #
1651
- # @example
1652
- # lib.current_command_version #=> [2, 42, 0]
1653
- #
1654
- # lib.compare_version_to(2, 41, 0) #=> 1
1655
- # lib.compare_version_to(2, 42, 0) #=> 0
1656
- # lib.compare_version_to(2, 43, 0) #=> -1
1657
- #
1658
- # @param other_version [Array<Object>] the other version to compare to
1659
- # @return [Integer] -1 if this version is less than other_version, 0 if equal, or 1 if greater than
1660
- #
1661
- def compare_version_to(*other_version)
1662
- current_command_version <=> other_version
1663
- end
1664
-
1665
- def required_command_version
1666
- [2, 28]
1667
- end
1668
-
1669
- def meets_required_version?
1670
- (current_command_version <=> required_command_version) >= 0
1671
- end
1672
-
1673
- def self.warn_if_old_command(lib) # rubocop:disable Naming/PredicateMethod
1674
- Git::Deprecation.warn('Git::Lib#warn_if_old_command is deprecated. Use meets_required_version?.')
1675
-
1676
- return true if @version_checked
1677
-
1678
- @version_checked = true
1679
- unless lib.meets_required_version?
1680
- warn "[WARNING] The git gem requires git #{lib.required_command_version.join('.')} or later, " \
1681
- "but only found #{lib.current_command_version.join('.')}. You should probably upgrade."
1682
- end
1683
- true
1684
- end
1685
-
1686
- COMMAND_ARG_DEFAULTS = {
1687
- out: nil,
1688
- err: nil,
1689
- normalize: true,
1690
- chomp: true,
1691
- merge: false,
1692
- chdir: nil,
1693
- timeout: nil # Don't set to Git.config.timeout here since it is mutable
1694
- }.freeze
1695
-
1696
- STATIC_GLOBAL_OPTS = %w[
1697
- -c core.quotePath=true
1698
- -c color.ui=false
1699
- -c color.advice=false
1700
- -c color.diff=false
1701
- -c color.grep=false
1702
- -c color.push=false
1703
- -c color.remote=false
1704
- -c color.showBranch=false
1705
- -c color.status=false
1706
- -c color.transport=false
1707
- ].freeze
1708
-
1709
- LOG_OPTION_MAP = [
1710
- { type: :static, flag: '--no-color' },
1711
- { keys: [:all], flag: '--all', type: :boolean },
1712
- { keys: [:cherry], flag: '--cherry', type: :boolean },
1713
- { keys: [:since], flag: '--since', type: :valued_equals },
1714
- { keys: [:until], flag: '--until', type: :valued_equals },
1715
- { keys: [:grep], flag: '--grep', type: :valued_equals },
1716
- { keys: [:author], flag: '--author', type: :valued_equals },
1717
- { keys: [:count], flag: '--max-count', type: :valued_equals },
1718
- { keys: [:between], type: :custom, builder: ->(value) { "#{value[0]}..#{value[1]}" if value } }
1719
- ].freeze
1720
-
1721
- FSCK_OBJECT_PATTERN = /\A(dangling|missing|unreachable) (\w+) ([0-9a-f]{40})(?: \((.+)\))?\z/
1722
- FSCK_WARNING_PATTERN = /\Awarning in (\w+) ([0-9a-f]{40}): (.+)\z/
1723
- FSCK_ROOT_PATTERN = /\Aroot ([0-9a-f]{40})\z/
1724
- FSCK_TAGGED_PATTERN = /\Atagged (\w+) ([0-9a-f]{40}) \((.+)\) in ([0-9a-f]{40})\z/
1725
-
1726
- private_constant :FSCK_OBJECT_PATTERN, :FSCK_WARNING_PATTERN, :FSCK_ROOT_PATTERN, :FSCK_TAGGED_PATTERN
1727
-
1728
- private
1729
-
1730
- def parse_fsck_output(output)
1731
- result = { dangling: [], missing: [], unreachable: [], warnings: [], root: [], tagged: [] }
1732
- output.each_line { |line| parse_fsck_line(line.strip, result) }
1733
- Git::FsckResult.new(**result)
1734
- end
1735
-
1736
- def parse_fsck_line(line, result)
1737
- parse_fsck_object_line(line, result) ||
1738
- parse_fsck_warning_line(line, result) ||
1739
- parse_fsck_root_line(line, result) ||
1740
- parse_fsck_tagged_line(line, result)
1741
- end
1742
-
1743
- def parse_fsck_object_line(line, result)
1744
- return unless (match = FSCK_OBJECT_PATTERN.match(line))
1745
-
1746
- result[match[1].to_sym] << Git::FsckObject.new(type: match[2].to_sym, sha: match[3], name: match[4])
1747
- end
1748
-
1749
- def parse_fsck_warning_line(line, result)
1750
- return unless (match = FSCK_WARNING_PATTERN.match(line))
1751
-
1752
- result[:warnings] << Git::FsckObject.new(type: match[1].to_sym, sha: match[2], message: match[3])
1753
- end
1754
-
1755
- def parse_fsck_root_line(line, result)
1756
- return unless (match = FSCK_ROOT_PATTERN.match(line))
1757
-
1758
- result[:root] << Git::FsckObject.new(type: :commit, sha: match[1])
1759
- end
1760
-
1761
- def parse_fsck_tagged_line(line, result)
1762
- return unless (match = FSCK_TAGGED_PATTERN.match(line))
1763
-
1764
- result[:tagged] << Git::FsckObject.new(type: match[1].to_sym, sha: match[2], name: match[3])
1765
- end
1766
-
1767
- def parse_diff_path_status(args)
1768
- command_lines('diff', *args).each_with_object({}) do |line, memo|
1769
- status, path = split_status_line(line)
1770
- memo[path] = status
1771
- end
1772
- end
1773
-
1774
- def build_checkout_positional_args(branch, opts)
1775
- args = []
1776
- if opts[:new_branch] || opts[:b]
1777
- args.push('-b', branch)
1778
- args << opts[:start_point] if opts[:start_point]
1779
- elsif branch
1780
- args << branch
1781
- end
1782
- args
1783
- end
1784
-
1785
- def build_args(opts, option_map)
1786
- Git::ArgsBuilder.new(opts, option_map).build
1787
- end
1788
-
1789
- def initialize_from_base(base_object)
1790
- @git_dir = base_object.repo.to_s
1791
- @git_index_file = base_object.index&.to_s
1792
- @git_work_dir = base_object.dir&.to_s
1793
- @git_ssh = base_object.git_ssh
1794
- end
1795
-
1796
- def initialize_from_hash(base_hash)
1797
- @git_dir = base_hash[:repository]
1798
- @git_index_file = base_hash[:index]
1799
- @git_work_dir = base_hash[:working_directory]
1800
- @git_ssh = base_hash.key?(:git_ssh) ? base_hash[:git_ssh] : :use_global_config
1801
- end
1802
-
1803
- def return_base_opts_from_clone(clone_dir, opts)
1804
- base_opts = {}
1805
- base_opts[:repository] = clone_dir if opts[:bare] || opts[:mirror]
1806
- base_opts[:working_directory] = clone_dir unless opts[:bare] || opts[:mirror]
1807
- base_opts[:log] = opts[:log] if opts[:log]
1808
- base_opts[:git_ssh] = opts[:git_ssh] if opts.key?(:git_ssh)
1809
- base_opts
1810
- end
1811
-
1812
- def process_commit_headers(data)
1813
- headers = { 'parent' => [] } # Pre-initialize for multiple parents
1814
- each_cat_file_header(data) do |key, value|
1815
- if key == 'parent'
1816
- headers['parent'] << value
1817
- else
1818
- headers[key] = value
1819
- end
1820
- end
1821
- headers
1822
- end
1823
-
1824
- def parse_branch_line(line, index, all_lines)
1825
- match_data = match_branch_line(line, index, all_lines)
1826
-
1827
- return nil if match_data[:not_a_branch] || match_data[:detached_ref]
1828
-
1829
- format_branch_data(match_data)
1830
- end
1831
-
1832
- def match_branch_line(line, index, all_lines)
1833
- match_data = line.match(BRANCH_LINE_REGEXP)
1834
- raise Git::UnexpectedResultError, unexpected_branch_line_error(all_lines, line, index) unless match_data
1835
-
1836
- match_data
1837
- end
1838
-
1839
- def format_branch_data(match_data)
1840
- [
1841
- match_data[:refname],
1842
- !match_data[:current].nil?,
1843
- !match_data[:worktree].nil?,
1844
- match_data[:symref]
1845
- ]
1846
- end
1847
-
1848
- def unexpected_branch_line_error(lines, line, index)
1849
- <<~ERROR
1850
- Unexpected line in output from `git branch -a`, line #{index + 1}
1851
-
1852
- Full output:
1853
- #{lines.join("\n ")}
1854
-
1855
- Line #{index + 1}:
1856
- "#{line}"
1857
- ERROR
1858
- end
1859
-
1860
- def get_branch_state(branch_name)
1861
- command('rev-parse', '--verify', '--quiet', branch_name)
1862
- :active
1863
- rescue Git::FailedError => e
1864
- # An exit status of 1 with empty stderr from `rev-parse --verify`
1865
- # indicates a ref that exists but does not yet point to a commit.
1866
- raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
1867
-
1868
- :unborn
1869
- end
1870
-
1871
- def execute_grep_command(args)
1872
- command_lines('grep', *args)
1873
- rescue Git::FailedError => e
1874
- # `git grep` returns 1 when no lines are selected.
1875
- raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
1876
-
1877
- [] # Return an empty array for "no matches found"
1878
- end
1879
-
1880
- def parse_grep_output(lines)
1881
- lines.each_with_object(Hash.new { |h, k| h[k] = [] }) do |line, hsh|
1882
- match = line.match(/\A(.*?):(\d+):(.*)/)
1883
- next unless match
1884
-
1885
- _full, filename, line_num, text = match.to_a
1886
- hsh[filename] << [line_num.to_i, text]
1887
- end
1888
- end
1889
-
1890
- def parse_diff_stats_output(lines)
1891
- file_stats = parse_stat_lines(lines)
1892
- build_final_stats_hash(file_stats)
1893
- end
1894
-
1895
- def parse_stat_lines(lines)
1896
- lines.map do |line|
1897
- insertions_s, deletions_s, filename = split_status_line(line)
1898
- {
1899
- filename: filename,
1900
- insertions: insertions_s.to_i,
1901
- deletions: deletions_s.to_i
1902
- }
1903
- end
1904
- end
1905
-
1906
- def split_status_line(line)
1907
- parts = line.split("\t")
1908
- parts[-1] = unescape_quoted_path(parts[-1]) if parts.any?
1909
- parts
1910
- end
1911
-
1912
- def build_final_stats_hash(file_stats)
1913
- {
1914
- total: build_total_stats(file_stats),
1915
- files: build_files_hash(file_stats)
1916
- }
1917
- end
1918
-
1919
- def build_total_stats(file_stats)
1920
- insertions = file_stats.sum { |s| s[:insertions] }
1921
- deletions = file_stats.sum { |s| s[:deletions] }
1922
- {
1923
- insertions: insertions,
1924
- deletions: deletions,
1925
- lines: insertions + deletions,
1926
- files: file_stats.size
1927
- }
1928
- end
1929
-
1930
- def build_files_hash(file_stats)
1931
- file_stats.to_h { |s| [s[:filename], s.slice(:insertions, :deletions)] }
1932
- end
1933
-
1934
- def parse_ls_remote_output(lines)
1935
- lines.each_with_object(Hash.new { |h, k| h[k] = {} }) do |line, hsh|
1936
- type, name, value = parse_ls_remote_line(line)
1937
- if name
1938
- hsh[type][name] = value
1939
- else # Handles the HEAD entry, which has no name
1940
- hsh[type].update(value)
1941
- end
1942
- end
1943
- end
1944
-
1945
- def parse_ls_remote_line(line)
1946
- sha, info = line.split("\t", 2)
1947
- ref, type, name = info.split('/', 3)
1948
-
1949
- type ||= 'head'
1950
- type = 'branches' if type == 'heads'
1951
-
1952
- value = { ref: ref, sha: sha }
1953
-
1954
- [type, name, value]
1955
- end
1956
-
1957
- def stash_log_lines
1958
- path = File.join(@git_dir, 'logs/refs/stash')
1959
- return [] unless File.exist?(path)
1960
-
1961
- File.readlines(path, chomp: true)
1962
- end
1963
-
1964
- def parse_stash_log_line(line, index)
1965
- full_message = line.split("\t", 2).last
1966
- match_data = full_message.match(/^[^:]+:(.*)$/)
1967
- message = match_data ? match_data[1] : full_message
1968
-
1969
- [index, message.strip]
1970
- end
1971
-
1972
- # Writes the staged content of a conflicted file to an IO stream
1973
- #
1974
- # @param path [String] the path to the file in the index
1975
- #
1976
- # @param stage [Integer] the stage of the file to show (e.g., 2 for 'ours', 3 for 'theirs')
1977
- #
1978
- # @param out_io [IO] the IO object to write the staged content to
1979
- #
1980
- # @return [IO] the IO object that was written to
1981
- #
1982
- def write_staged_content(path, stage, out_io)
1983
- command('show', ":#{stage}:#{path}", out: out_io)
1984
- out_io
1985
- end
1986
-
1987
- def validate_tag_options!(opts)
1988
- is_annotated = opts[:a] || opts[:annotate]
1989
- has_message = opts[:m] || opts[:message]
1990
-
1991
- return unless is_annotated && !has_message
1992
-
1993
- raise ArgumentError, 'Cannot create an annotated tag without a message.'
1994
- end
1995
-
1996
- def normalize_push_args(remote, branch, opts)
1997
- if branch.is_a?(Hash)
1998
- opts = branch
1999
- branch = nil
2000
- elsif remote.is_a?(Hash)
2001
- opts = remote
2002
- remote = nil
2003
- end
2004
-
2005
- opts ||= {}
2006
- # Backwards compatibility for `push(remote, branch, true)`
2007
- opts = { tags: opts } if [true, false].include?(opts)
2008
- [remote, branch, opts]
2009
- end
2010
-
2011
- def build_push_args(remote, branch, opts)
2012
- # Build the simple flags using the ArgsBuilder
2013
- args = build_args(opts, PUSH_OPTION_MAP)
2014
-
2015
- # Manually handle the flag with external dependencies and positional args
2016
- args << '--all' if opts[:all] && remote
2017
- args << remote if remote
2018
- args << branch if branch
2019
- args
2020
- end
2021
-
2022
- def temp_file_name
2023
- tempfile = Tempfile.new('archive')
2024
- file = tempfile.path
2025
- tempfile.close! # Prevents Ruby from deleting the file on garbage collection
2026
- file
2027
- end
2028
-
2029
- def parse_archive_format_options(opts)
2030
- format = opts[:format] || 'zip'
2031
- gzip = opts[:add_gzip] == true || format == 'tgz'
2032
- format = 'tar' if format == 'tgz'
2033
- [format, gzip]
2034
- end
2035
-
2036
- def apply_gzip(file)
2037
- file_content = File.read(file)
2038
- Zlib::GzipWriter.open(file) { |gz| gz.write(file_content) }
2039
- end
2040
-
2041
- def command_lines(cmd, *opts, chdir: nil)
2042
- cmd_op = command(cmd, *opts, chdir: chdir)
2043
- op = if cmd_op.encoding.name == 'UTF-8'
2044
- cmd_op
2045
- else
2046
- cmd_op.encode('UTF-8', 'binary', invalid: :replace, undef: :replace)
2047
- end
2048
- op.split("\n")
2049
- end
2050
-
2051
- # Returns a hash of environment variable overrides for git commands
2052
- #
2053
- # This method builds a hash of environment variables that control git's behavior,
2054
- # such as the git directory, working tree, and index file locations.
2055
- #
2056
- # @param additional_overrides [Hash] additional environment variables to set or unset
2057
- #
2058
- # Keys should be environment variable names (String) and values should be either:
2059
- # * A String value to set the environment variable
2060
- # * `nil` to unset the environment variable
2061
- #
2062
- # Per Process.spawn semantics, setting a key to `nil` will unset that environment
2063
- # variable, removing it from the environment passed to the git command.
2064
- #
2065
- # @return [Hash<String, String|nil>] environment variable overrides
2066
- #
2067
- # @example Basic usage with default environment variables
2068
- # env_overrides
2069
- # # => { 'GIT_DIR' => '/path/to/.git', 'GIT_WORK_TREE' => '/path/to/worktree', ... }
2070
- #
2071
- # @example Adding a custom environment variable
2072
- # env_overrides('GIT_TRACE' => '1')
2073
- # # => { 'GIT_DIR' => '/path/to/.git', ..., 'GIT_TRACE' => '1' }
2074
- #
2075
- # @example Unsetting an environment variable (used by worktree_command_line)
2076
- # env_overrides('GIT_INDEX_FILE' => nil)
2077
- # # => { 'GIT_DIR' => '/path/to/.git', 'GIT_WORK_TREE' => '/path/to/worktree',
2078
- # # 'GIT_INDEX_FILE' => nil, 'GIT_SSH' => <git_ssh_value>, 'LC_ALL' => 'en_US.UTF-8' }
2079
- # # When passed to Process.spawn, GIT_INDEX_FILE will be unset in the environment
2080
- #
2081
- # @see https://ruby-doc.org/core/Process.html#method-c-spawn Process.spawn
2082
- #
2083
- # @api private
2084
- #
2085
- def env_overrides(**additional_overrides)
2086
- {
2087
- 'GIT_DIR' => @git_dir,
2088
- 'GIT_WORK_TREE' => @git_work_dir,
2089
- 'GIT_INDEX_FILE' => @git_index_file,
2090
- 'GIT_SSH' => resolved_git_ssh,
2091
- 'LC_ALL' => 'en_US.UTF-8'
2092
- }.merge(additional_overrides)
2093
- end
2094
-
2095
- # Resolve the git_ssh value to use for this instance
2096
- #
2097
- # @return [String, nil] the resolved git_ssh value
2098
- #
2099
- # Returns the global config value if @git_ssh is the sentinel :use_global_config,
2100
- # otherwise returns @git_ssh (which may be nil or a string)
2101
- #
2102
- # @api private
2103
- #
2104
- def resolved_git_ssh
2105
- return Git::Base.config.git_ssh if @git_ssh == :use_global_config
2106
-
2107
- @git_ssh
2108
- end
2109
-
2110
- def global_opts
2111
- [].tap do |global_opts|
2112
- global_opts << "--git-dir=#{@git_dir}" unless @git_dir.nil?
2113
- global_opts << "--work-tree=#{@git_work_dir}" unless @git_work_dir.nil?
2114
- global_opts.concat(STATIC_GLOBAL_OPTS)
2115
- end
2116
- end
2117
-
2118
- def command_line
2119
- @command_line ||=
2120
- Git::CommandLine.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
2121
- end
2122
-
2123
- # Returns a command line instance without GIT_INDEX_FILE for worktree commands
2124
- #
2125
- # Git worktrees manage their own index files and setting GIT_INDEX_FILE
2126
- # causes corruption of both the main worktree and new worktree indexes.
2127
- #
2128
- # @return [Git::CommandLine]
2129
- # @api private
2130
- #
2131
- def worktree_command_line
2132
- @worktree_command_line ||=
2133
- Git::CommandLine.new(env_overrides('GIT_INDEX_FILE' => nil), Git::Base.config.binary_path, global_opts,
2134
- @logger)
2135
- end
2136
-
2137
- # @overload worktree_command(*args, **options_hash)
2138
- # Runs a git worktree command and returns the output
2139
- #
2140
- # This method is similar to #command but uses a command line instance
2141
- # that excludes GIT_INDEX_FILE from the environment to prevent index corruption.
2142
- #
2143
- # @param args [Array<String>] the command arguments
2144
- # @param options_hash [Hash] the options to pass to the command
2145
- #
2146
- # @return [String] the command's stdout
2147
- #
2148
- # @see #command
2149
- #
2150
- # @api private
2151
- #
2152
- def worktree_command(*, **options_hash)
2153
- options_hash = COMMAND_ARG_DEFAULTS.merge(options_hash)
2154
- options_hash[:timeout] ||= Git.config.timeout
2155
-
2156
- extra_options = options_hash.keys - COMMAND_ARG_DEFAULTS.keys
2157
- raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
2158
-
2159
- result = worktree_command_line.run(*, **options_hash)
2160
- result.stdout
2161
- end
2162
-
2163
- # Runs a git command and returns the output
2164
- #
2165
- # Additional args are passed to the command line. They should exclude the 'git'
2166
- # command itself and global options. Remember to splat the the arguments if given
2167
- # as an array.
2168
- #
2169
- # For example, to run `git log --pretty=oneline`, you would create the array
2170
- # `args = ['log', '--pretty=oneline']` and call `command(*args)`.
2171
- #
2172
- # @param options_hash [Hash] the options to pass to the command
2173
- # @option options_hash [IO, String, #write, nil] :out the destination for captured stdout
2174
- # @option options_hash [IO, String, #write, nil] :err the destination for captured stderr
2175
- # @option options_hash [Boolean] :normalize true to normalize the output encoding to UTF-8
2176
- # @option options_hash [Boolean] :chomp true to remove trailing newlines from the output
2177
- # @option options_hash [Boolean] :merge true to merge stdout and stderr into a single output
2178
- # @option options_hash [String, nil] :chdir the directory to run the command in
2179
- # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete
2180
- #
2181
- # If timeout is nil, the global timeout from {Git::Config} is used.
2182
- #
2183
- # If timeout is zero, the timeout will not be enforced.
2184
- #
2185
- # If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
2186
- #
2187
- # If the command does not respond to SIGKILL, it will hang this method.
2188
- #
2189
- # @see Git::CommandLine#run
2190
- #
2191
- # @return [String] the command's stdout (or merged stdout and stderr if `merge`
2192
- # is true)
2193
- #
2194
- # @raise [ArgumentError] if an unknown option is passed
2195
- #
2196
- # @raise [Git::FailedError] if the command failed
2197
- #
2198
- # @raise [Git::SignaledError] if the command was signaled
2199
- #
2200
- # @raise [Git::TimeoutError] if the command times out
2201
- #
2202
- # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
2203
- #
2204
- # The exception's `result` attribute is a {Git::CommandLineResult} which will
2205
- # contain the result of the command including the exit status, stdout, and
2206
- # stderr.
2207
- #
2208
- # @api private
2209
- #
2210
- def command(*, **options_hash)
2211
- options_hash = COMMAND_ARG_DEFAULTS.merge(options_hash)
2212
- options_hash[:timeout] ||= Git.config.timeout
2213
-
2214
- extra_options = options_hash.keys - COMMAND_ARG_DEFAULTS.keys
2215
- raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
2216
-
2217
- result = command_line.run(*, **options_hash)
2218
- result.stdout
2219
- end
2220
-
2221
- # Takes the diff command line output (as Array) and parse it into a Hash
2222
- #
2223
- # @param [String] diff_command the diff commadn to be used
2224
- # @param [Array] opts the diff options to be used
2225
- # @return [Hash] the diff as Hash
2226
- def diff_as_hash(diff_command, opts = [])
2227
- # update index before diffing to avoid spurious diffs
2228
- command('status')
2229
- command_lines(diff_command, *opts).each_with_object({}) do |line, memo|
2230
- info, file = split_status_line(line)
2231
- mode_src, mode_dest, sha_src, sha_dest, type = info.split
2232
-
2233
- memo[file] = {
2234
- mode_index: mode_dest, mode_repo: mode_src.to_s[1, 7],
2235
- path: file, sha_repo: sha_src, sha_index: sha_dest,
2236
- type: type
2237
- }
2238
- end
2239
- end
2240
-
2241
- # Returns an array holding the common options for the log commands
2242
- #
2243
- # @param [Hash] opts the given options
2244
- # @return [Array] the set of common options that the log command will use
2245
- def log_common_options(opts)
2246
- if opts[:count] && !opts[:count].is_a?(Integer)
2247
- raise ArgumentError, "The log count option must be an Integer but was #{opts[:count].inspect}"
2248
- end
2249
-
2250
- build_args(opts, LOG_OPTION_MAP)
2251
- end
2252
-
2253
- # Retrurns an array holding path options for the log commands
2254
- #
2255
- # @param [Hash] opts the given options
2256
- # @return [Array] the set of path options that the log command will use
2257
- def log_path_options(opts)
2258
- arr_opts = []
2259
-
2260
- arr_opts << opts[:object] if opts[:object].is_a? String
2261
- if opts[:path_limiter]
2262
- arr_opts << '--'
2263
- arr_opts += Array(opts[:path_limiter])
2264
- end
2265
- arr_opts
2266
- end
2267
-
2268
- def log_or_empty_on_unborn
2269
- yield
2270
- rescue Git::FailedError => e
2271
- raise unless e.result.status.exitstatus == 128 &&
2272
- e.result.stderr =~ /does not have any commits yet/
2273
-
2274
- []
2275
- end
2276
- end
2277
- end