git 4.4.2 → 5.0.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +67 -2705
- data/.github/pull_request_template.md +3 -1
- data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
- data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
- data/.github/skills/command-implementation/REFERENCE.md +993 -0
- data/.github/skills/command-implementation/SKILL.md +229 -0
- data/.github/skills/command-test-conventions/SKILL.md +660 -0
- data/.github/skills/command-yard-documentation/SKILL.md +426 -0
- data/.github/skills/dependency-management/SKILL.md +72 -0
- data/.github/skills/development-workflow/SKILL.md +506 -0
- data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
- data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
- data/.github/skills/facade-implementation/REFERENCE.md +840 -0
- data/.github/skills/facade-implementation/SKILL.md +260 -0
- data/.github/skills/facade-test-conventions/SKILL.md +380 -0
- data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
- data/.github/skills/make-skill-template/SKILL.md +176 -0
- data/.github/skills/pr-readiness-review/SKILL.md +185 -0
- data/.github/skills/project-context/SKILL.md +313 -0
- data/.github/skills/pull-request-review/SKILL.md +168 -0
- data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
- data/.github/skills/release-management/SKILL.md +125 -0
- data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
- data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
- data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
- data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
- data/.github/skills/reviewing-skills/SKILL.md +189 -0
- data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
- data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
- data/.github/skills/test-debugging/SKILL.md +160 -0
- data/.github/skills/yard-documentation/SKILL.md +793 -0
- data/.github/workflows/continuous_integration.yml +3 -67
- data/.github/workflows/enforce_conventional_commits.yml +1 -1
- data/.github/workflows/experimental_continuous_integration.yml +2 -2
- data/.github/workflows/release.yml +3 -4
- data/.gitignore +8 -0
- data/.husky/pre-commit +13 -0
- data/.release-please-manifest.json +1 -1
- data/.rspec +3 -0
- data/.rubocop.yml +7 -3
- data/.rubocop_todo.yml +23 -5
- data/.yardopts +1 -0
- data/CHANGELOG.md +0 -73
- data/CONTRIBUTING.md +694 -53
- data/README.md +17 -17
- data/Rakefile +61 -9
- data/commitlint.test +4 -0
- data/git.gemspec +14 -8
- data/lib/git/args_builder.rb +0 -8
- data/lib/git/base.rb +485 -420
- data/lib/git/branch.rb +117 -59
- data/lib/git/branch_delete_failure.rb +31 -0
- data/lib/git/branch_delete_result.rb +63 -0
- data/lib/git/branch_info.rb +178 -0
- data/lib/git/branches.rb +130 -24
- data/lib/git/command_line/base.rb +245 -0
- data/lib/git/command_line/capturing.rb +249 -0
- data/lib/git/command_line/result.rb +96 -0
- data/lib/git/command_line/streaming.rb +194 -0
- data/lib/git/command_line.rb +43 -322
- data/lib/git/command_line_result.rb +4 -88
- data/lib/git/commands/add.rb +131 -0
- data/lib/git/commands/am/abort.rb +43 -0
- data/lib/git/commands/am/apply.rb +252 -0
- data/lib/git/commands/am/continue.rb +43 -0
- data/lib/git/commands/am/quit.rb +43 -0
- data/lib/git/commands/am/retry.rb +47 -0
- data/lib/git/commands/am/show_current_patch.rb +64 -0
- data/lib/git/commands/am/skip.rb +42 -0
- data/lib/git/commands/am.rb +33 -0
- data/lib/git/commands/apply.rb +237 -0
- data/lib/git/commands/archive/list_formats.rb +46 -0
- data/lib/git/commands/archive.rb +140 -0
- data/lib/git/commands/arguments.rb +3510 -0
- data/lib/git/commands/base.rb +403 -0
- data/lib/git/commands/branch/copy.rb +94 -0
- data/lib/git/commands/branch/create.rb +173 -0
- data/lib/git/commands/branch/delete.rb +80 -0
- data/lib/git/commands/branch/list.rb +162 -0
- data/lib/git/commands/branch/move.rb +94 -0
- data/lib/git/commands/branch/set_upstream.rb +86 -0
- data/lib/git/commands/branch/show_current.rb +49 -0
- data/lib/git/commands/branch/unset_upstream.rb +57 -0
- data/lib/git/commands/branch.rb +34 -0
- data/lib/git/commands/cat_file/batch.rb +364 -0
- data/lib/git/commands/cat_file/filtered.rb +105 -0
- data/lib/git/commands/cat_file/raw.rb +210 -0
- data/lib/git/commands/cat_file.rb +49 -0
- data/lib/git/commands/checkout/branch.rb +151 -0
- data/lib/git/commands/checkout/files.rb +115 -0
- data/lib/git/commands/checkout.rb +38 -0
- data/lib/git/commands/checkout_index.rb +105 -0
- data/lib/git/commands/clean.rb +100 -0
- data/lib/git/commands/clone.rb +240 -0
- data/lib/git/commands/commit.rb +272 -0
- data/lib/git/commands/commit_tree.rb +100 -0
- data/lib/git/commands/config_option_syntax/add.rb +83 -0
- data/lib/git/commands/config_option_syntax/get.rb +117 -0
- data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
- data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
- data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
- data/lib/git/commands/config_option_syntax/list.rb +107 -0
- data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
- data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
- data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
- data/lib/git/commands/config_option_syntax/set.rb +114 -0
- data/lib/git/commands/config_option_syntax/unset.rb +89 -0
- data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
- data/lib/git/commands/config_option_syntax.rb +56 -0
- data/lib/git/commands/describe.rb +155 -0
- data/lib/git/commands/diff.rb +656 -0
- data/lib/git/commands/diff_files.rb +518 -0
- data/lib/git/commands/diff_index.rb +496 -0
- data/lib/git/commands/fetch.rb +352 -0
- data/lib/git/commands/fsck.rb +136 -0
- data/lib/git/commands/gc.rb +132 -0
- data/lib/git/commands/grep.rb +338 -0
- data/lib/git/commands/init.rb +99 -0
- data/lib/git/commands/log.rb +632 -0
- data/lib/git/commands/ls_files.rb +191 -0
- data/lib/git/commands/ls_remote.rb +155 -0
- data/lib/git/commands/ls_tree.rb +131 -0
- data/lib/git/commands/maintenance/register.rb +75 -0
- data/lib/git/commands/maintenance/run.rb +104 -0
- data/lib/git/commands/maintenance/start.rb +66 -0
- data/lib/git/commands/maintenance/stop.rb +55 -0
- data/lib/git/commands/maintenance/unregister.rb +79 -0
- data/lib/git/commands/maintenance.rb +31 -0
- data/lib/git/commands/merge/abort.rb +44 -0
- data/lib/git/commands/merge/continue.rb +44 -0
- data/lib/git/commands/merge/quit.rb +46 -0
- data/lib/git/commands/merge/start.rb +245 -0
- data/lib/git/commands/merge.rb +28 -0
- data/lib/git/commands/merge_base.rb +86 -0
- data/lib/git/commands/mv.rb +77 -0
- data/lib/git/commands/name_rev.rb +114 -0
- data/lib/git/commands/pull.rb +377 -0
- data/lib/git/commands/push.rb +246 -0
- data/lib/git/commands/read_tree.rb +149 -0
- data/lib/git/commands/remote/add.rb +91 -0
- data/lib/git/commands/remote/get_url.rb +66 -0
- data/lib/git/commands/remote/list.rb +54 -0
- data/lib/git/commands/remote/prune.rb +61 -0
- data/lib/git/commands/remote/remove.rb +52 -0
- data/lib/git/commands/remote/rename.rb +69 -0
- data/lib/git/commands/remote/set_branches.rb +63 -0
- data/lib/git/commands/remote/set_head.rb +82 -0
- data/lib/git/commands/remote/set_url.rb +71 -0
- data/lib/git/commands/remote/set_url_add.rb +61 -0
- data/lib/git/commands/remote/set_url_delete.rb +64 -0
- data/lib/git/commands/remote/show.rb +71 -0
- data/lib/git/commands/remote/update.rb +72 -0
- data/lib/git/commands/remote.rb +42 -0
- data/lib/git/commands/repack.rb +277 -0
- data/lib/git/commands/reset.rb +147 -0
- data/lib/git/commands/rev_parse.rb +297 -0
- data/lib/git/commands/revert/abort.rb +45 -0
- data/lib/git/commands/revert/continue.rb +57 -0
- data/lib/git/commands/revert/quit.rb +47 -0
- data/lib/git/commands/revert/skip.rb +44 -0
- data/lib/git/commands/revert/start.rb +153 -0
- data/lib/git/commands/revert.rb +29 -0
- data/lib/git/commands/rm.rb +114 -0
- data/lib/git/commands/show.rb +632 -0
- data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
- data/lib/git/commands/show_ref/exists.rb +78 -0
- data/lib/git/commands/show_ref/list.rb +145 -0
- data/lib/git/commands/show_ref/verify.rb +120 -0
- data/lib/git/commands/show_ref.rb +42 -0
- data/lib/git/commands/stash/apply.rb +75 -0
- data/lib/git/commands/stash/branch.rb +65 -0
- data/lib/git/commands/stash/clear.rb +41 -0
- data/lib/git/commands/stash/create.rb +58 -0
- data/lib/git/commands/stash/drop.rb +67 -0
- data/lib/git/commands/stash/list.rb +39 -0
- data/lib/git/commands/stash/pop.rb +78 -0
- data/lib/git/commands/stash/push.rb +103 -0
- data/lib/git/commands/stash/show.rb +149 -0
- data/lib/git/commands/stash/store.rb +63 -0
- data/lib/git/commands/stash.rb +38 -0
- data/lib/git/commands/status.rb +169 -0
- data/lib/git/commands/symbolic_ref/delete.rb +68 -0
- data/lib/git/commands/symbolic_ref/read.rb +95 -0
- data/lib/git/commands/symbolic_ref/update.rb +76 -0
- data/lib/git/commands/symbolic_ref.rb +38 -0
- data/lib/git/commands/tag/create.rb +139 -0
- data/lib/git/commands/tag/delete.rb +55 -0
- data/lib/git/commands/tag/list.rb +143 -0
- data/lib/git/commands/tag/verify.rb +71 -0
- data/lib/git/commands/tag.rb +26 -0
- data/lib/git/commands/update_ref/batch.rb +140 -0
- data/lib/git/commands/update_ref/delete.rb +92 -0
- data/lib/git/commands/update_ref/update.rb +106 -0
- data/lib/git/commands/update_ref.rb +42 -0
- data/lib/git/commands/version.rb +52 -0
- data/lib/git/commands/worktree/add.rb +140 -0
- data/lib/git/commands/worktree/list.rb +64 -0
- data/lib/git/commands/worktree/lock.rb +58 -0
- data/lib/git/commands/worktree/management_base.rb +51 -0
- data/lib/git/commands/worktree/move.rb +66 -0
- data/lib/git/commands/worktree/prune.rb +67 -0
- data/lib/git/commands/worktree/remove.rb +63 -0
- data/lib/git/commands/worktree/repair.rb +76 -0
- data/lib/git/commands/worktree/unlock.rb +47 -0
- data/lib/git/commands/worktree.rb +43 -0
- data/lib/git/commands/write_tree.rb +68 -0
- data/lib/git/commands.rb +89 -0
- data/lib/git/detached_head_info.rb +54 -0
- data/lib/git/diff.rb +297 -7
- data/lib/git/diff_file_numstat_info.rb +29 -0
- data/lib/git/diff_file_patch_info.rb +134 -0
- data/lib/git/diff_file_raw_info.rb +127 -0
- data/lib/git/diff_info.rb +169 -0
- data/lib/git/diff_path_status.rb +78 -19
- data/lib/git/diff_result.rb +32 -0
- data/lib/git/diff_stats.rb +59 -14
- data/lib/git/dirstat_info.rb +86 -0
- data/lib/git/errors.rb +65 -2
- data/lib/git/execution_context/global.rb +56 -0
- data/lib/git/execution_context/repository.rb +147 -0
- data/lib/git/execution_context.rb +482 -0
- data/lib/git/file_ref.rb +74 -0
- data/lib/git/fsck_object.rb +9 -9
- data/lib/git/fsck_result.rb +1 -1
- data/lib/git/lib.rb +1606 -1084
- data/lib/git/log.rb +15 -2
- data/lib/git/object.rb +92 -22
- data/lib/git/parsers/branch.rb +224 -0
- data/lib/git/parsers/cat_file.rb +111 -0
- data/lib/git/parsers/diff.rb +585 -0
- data/lib/git/parsers/fsck.rb +133 -0
- data/lib/git/parsers/grep.rb +42 -0
- data/lib/git/parsers/ls_tree.rb +58 -0
- data/lib/git/parsers/stash.rb +208 -0
- data/lib/git/parsers/tag.rb +257 -0
- data/lib/git/remote.rb +133 -9
- data/lib/git/repository/branching.rb +572 -0
- data/lib/git/repository/committing.rb +191 -0
- data/lib/git/repository/configuring.rb +156 -0
- data/lib/git/repository/diffing.rb +775 -0
- data/lib/git/repository/inspecting.rb +153 -0
- data/lib/git/repository/logging.rb +247 -0
- data/lib/git/repository/merging.rb +295 -0
- data/lib/git/repository/object_operations.rb +1101 -0
- data/lib/git/repository/path_resolver.rb +207 -0
- data/lib/git/repository/remote_operations.rb +753 -0
- data/lib/git/repository/shared_private.rb +51 -0
- data/lib/git/repository/staging.rb +390 -0
- data/lib/git/repository/stashing.rb +107 -0
- data/lib/git/repository/status_operations.rb +180 -0
- data/lib/git/repository/worktree_operations.rb +159 -0
- data/lib/git/repository.rb +264 -1
- data/lib/git/stash.rb +85 -4
- data/lib/git/stash_info.rb +104 -0
- data/lib/git/stashes.rb +130 -13
- data/lib/git/status.rb +226 -18
- data/lib/git/tag_delete_failure.rb +31 -0
- data/lib/git/tag_delete_result.rb +63 -0
- data/lib/git/tag_info.rb +105 -0
- data/lib/git/version.rb +109 -2
- data/lib/git/version_constraint.rb +81 -0
- data/lib/git/worktree.rb +120 -5
- data/lib/git/worktrees.rb +107 -7
- data/lib/git.rb +117 -56
- data/redesign/1_architecture_existing.md +54 -18
- data/redesign/2_architecture_redesign.md +365 -46
- data/redesign/3_architecture_implementation.md +1451 -54
- data/tasks/gem_tasks.rake +4 -0
- data/tasks/npm_tasks.rake +7 -0
- data/tasks/rspec.rake +48 -0
- data/tasks/test.rake +13 -1
- data/tasks/yard.rake +34 -7
- metadata +349 -20
- data/lib/git/index.rb +0 -6
- data/lib/git/path.rb +0 -38
- data/lib/git/working_directory.rb +0 -6
- /data/{release-please-config.json → .release-please-config.json} +0 -0
data/lib/git/lib.rb
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'args_builder'
|
|
4
|
+
require_relative 'commands'
|
|
4
5
|
|
|
5
6
|
require 'git/command_line'
|
|
6
7
|
require 'git/errors'
|
|
8
|
+
require 'git/parsers/branch'
|
|
9
|
+
require 'git/parsers/fsck'
|
|
10
|
+
require 'git/parsers/grep'
|
|
11
|
+
require 'git/parsers/stash'
|
|
12
|
+
require 'git/parsers/tag'
|
|
13
|
+
require 'git/url'
|
|
7
14
|
require 'logger'
|
|
8
15
|
require 'pathname'
|
|
9
16
|
require 'pp'
|
|
@@ -16,6 +23,10 @@ module Git
|
|
|
16
23
|
# Internal git operations
|
|
17
24
|
# @api private
|
|
18
25
|
class Lib
|
|
26
|
+
# Thread-safe cache for git versions, keyed by binary path
|
|
27
|
+
@git_version_cache_mutex = Mutex.new
|
|
28
|
+
@git_version_cache = {}
|
|
29
|
+
|
|
19
30
|
# The path to the Git working copy. The default is '"./.git"'.
|
|
20
31
|
#
|
|
21
32
|
# @return [Pathname] the path to the Git working copy.
|
|
@@ -74,52 +85,29 @@ module Git
|
|
|
74
85
|
end
|
|
75
86
|
end
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
#
|
|
88
|
+
# Creates or reinitializes the repository in the current directory
|
|
89
|
+
#
|
|
90
|
+
# This is a low-level method that just runs `git init` with the given options.
|
|
91
|
+
# For full repository initialization including directory creation and path
|
|
92
|
+
# resolution, use Git.init instead.
|
|
93
|
+
#
|
|
94
|
+
# @param opts [Hash] command options
|
|
95
|
+
#
|
|
96
|
+
# @option opts [Boolean, nil] :bare (nil) create a bare repository
|
|
97
|
+
#
|
|
98
|
+
# @option opts [String, nil] :initial_branch (nil) use the specified name for the initial branch
|
|
99
|
+
#
|
|
100
|
+
# @option opts [String, nil] :separate_git_dir (nil) path to put the .git directory (`--separate-git-dir`)
|
|
83
101
|
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# :initial_branch
|
|
102
|
+
# @option opts [String, nil] :repository (nil) deprecated — use `:separate_git_dir` instead
|
|
103
|
+
#
|
|
104
|
+
# @return [String] the command output
|
|
88
105
|
#
|
|
89
106
|
def init(opts = {})
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
CLONE_OPTION_MAP = [
|
|
95
|
-
{ keys: [:bare], flag: '--bare', type: :boolean },
|
|
96
|
-
{ keys: [:recursive], flag: '--recursive', type: :boolean },
|
|
97
|
-
{ keys: [:mirror], flag: '--mirror', type: :boolean },
|
|
98
|
-
{ keys: [:branch], flag: '--branch', type: :valued_space },
|
|
99
|
-
{ keys: [:filter], flag: '--filter', type: :valued_space },
|
|
100
|
-
{ keys: %i[remote origin], flag: '--origin', type: :valued_space },
|
|
101
|
-
{ keys: [:config], flag: '--config', type: :repeatable_valued_space },
|
|
102
|
-
{
|
|
103
|
-
keys: [:single_branch],
|
|
104
|
-
type: :custom,
|
|
105
|
-
validator: ->(value) { [nil, true, false].include?(value) },
|
|
106
|
-
builder: lambda do |value|
|
|
107
|
-
case value
|
|
108
|
-
when true
|
|
109
|
-
['--single-branch']
|
|
110
|
-
when false
|
|
111
|
-
['--no-single-branch']
|
|
112
|
-
else
|
|
113
|
-
[]
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
keys: [:depth],
|
|
119
|
-
type: :custom,
|
|
120
|
-
builder: ->(value) { ['--depth', value.to_i] if value }
|
|
121
|
-
}
|
|
122
|
-
].freeze
|
|
107
|
+
opts = opts.dup
|
|
108
|
+
opts[:separate_git_dir] ||= opts.delete(:repository)
|
|
109
|
+
Git::Commands::Init.new(self).call(**opts).stdout
|
|
110
|
+
end
|
|
123
111
|
|
|
124
112
|
# Clones a repository into a newly created directory
|
|
125
113
|
#
|
|
@@ -132,28 +120,42 @@ module Git
|
|
|
132
120
|
#
|
|
133
121
|
# @param [Hash] opts the options for this command
|
|
134
122
|
#
|
|
135
|
-
# @option opts [Boolean] :bare (
|
|
123
|
+
# @option opts [Boolean, nil] :bare (nil) if true, clone as a bare repository
|
|
136
124
|
#
|
|
137
|
-
# @option opts [String] :branch the branch to checkout
|
|
125
|
+
# @option opts [String, nil] :branch (nil) the branch to checkout
|
|
138
126
|
#
|
|
139
|
-
# @option opts [String, Array] :config one or more configuration options to set
|
|
127
|
+
# @option opts [String, Array<String>, nil] :config (nil) one or more configuration options to set
|
|
140
128
|
#
|
|
141
|
-
# @option opts [Integer] :depth the number of commits back to pull
|
|
129
|
+
# @option opts [Integer, nil] :depth (nil) the number of commits back to pull
|
|
142
130
|
#
|
|
143
|
-
# @option opts [String] :filter specify partial clone
|
|
131
|
+
# @option opts [String, nil] :filter (nil) specify partial clone
|
|
144
132
|
#
|
|
145
|
-
# @option opts [String] :
|
|
133
|
+
# @option opts [String, nil] :git_ssh (nil) SSH command or binary to use for git over SSH
|
|
146
134
|
#
|
|
147
|
-
# @option opts [
|
|
135
|
+
# @option opts [Logger, nil] :log (nil) Logger instance to use for git operations
|
|
148
136
|
#
|
|
149
|
-
# @option opts [
|
|
137
|
+
# @option opts [Boolean, nil] :mirror (nil) set up a mirror of the source repository
|
|
150
138
|
#
|
|
151
|
-
# @option opts [String] :
|
|
139
|
+
# @option opts [String, nil] :origin (nil) the name of the remote
|
|
152
140
|
#
|
|
153
|
-
# @option opts [
|
|
154
|
-
# within, using their default settings
|
|
141
|
+
# @option opts [String, nil] :chdir (nil) run `git clone` from this directory
|
|
155
142
|
#
|
|
156
|
-
#
|
|
143
|
+
# When given, `directory` (or the repository basename when `directory` is nil)
|
|
144
|
+
# is resolved relative to `:chdir`, just as if you had `cd`'d into it before
|
|
145
|
+
# running `git clone`. The returned path is the join of `:chdir` and the
|
|
146
|
+
# cloned directory path.
|
|
147
|
+
#
|
|
148
|
+
# @option opts [String] :path deprecated: use `:chdir` instead.
|
|
149
|
+
#
|
|
150
|
+
# @option opts [String] :remote deprecated: use `:origin` instead.
|
|
151
|
+
#
|
|
152
|
+
# @option opts [Boolean, String, Array<String>, nil] :recurse_submodules (nil) initialize
|
|
153
|
+
# submodules after cloning; pass `true` for all submodules, or a pathspec string/array
|
|
154
|
+
# for a subset
|
|
155
|
+
#
|
|
156
|
+
# @option opts [Boolean, String, Array<String>, nil] :recursive (nil) deprecated: use `:recurse_submodules` instead
|
|
157
|
+
#
|
|
158
|
+
# @option opts [Numeric, nil] :timeout (nil) the number of seconds to wait for the
|
|
157
159
|
# command to complete
|
|
158
160
|
#
|
|
159
161
|
# See {Git::Lib#command} for more information about :timeout
|
|
@@ -162,16 +164,16 @@ module Git
|
|
|
162
164
|
#
|
|
163
165
|
# @todo make this work with SSH password or auth_key
|
|
164
166
|
#
|
|
165
|
-
def clone(repository_url, directory, opts = {})
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
167
|
+
def clone(repository_url, directory = nil, opts = {})
|
|
168
|
+
opts = opts.dup
|
|
169
|
+
deprecate_clone_options!(opts)
|
|
170
|
+
chdir = opts.delete(:chdir)
|
|
171
|
+
execution_opts = extract_clone_execution_context_opts(opts)
|
|
172
|
+
opts[:chdir] = chdir if chdir
|
|
173
|
+
command_line_result = Git::Commands::Clone.new(self).call(repository_url, directory, **opts)
|
|
174
|
+
result = build_clone_result(command_line_result, execution_opts)
|
|
175
|
+
prefix_clone_result_paths!(result, chdir)
|
|
176
|
+
result
|
|
175
177
|
end
|
|
176
178
|
|
|
177
179
|
# Returns the name of the default branch of the given repository
|
|
@@ -181,7 +183,7 @@ module Git
|
|
|
181
183
|
# @return [String] the name of the default branch
|
|
182
184
|
#
|
|
183
185
|
def repository_default_branch(repository)
|
|
184
|
-
output =
|
|
186
|
+
output = Git::Commands::LsRemote.new(self).call(repository, 'HEAD', symref: true).stdout
|
|
185
187
|
|
|
186
188
|
match_data = output.match(%r{^ref: refs/remotes/origin/(?<default_branch>[^\t]+)\trefs/remotes/origin/HEAD$})
|
|
187
189
|
return match_data[:default_branch] if match_data
|
|
@@ -194,29 +196,6 @@ module Git
|
|
|
194
196
|
|
|
195
197
|
## READ COMMANDS ##
|
|
196
198
|
|
|
197
|
-
# The map defining how to translate user options to git command arguments.
|
|
198
|
-
DESCRIBE_OPTION_MAP = [
|
|
199
|
-
{ keys: [:all], flag: '--all', type: :boolean },
|
|
200
|
-
{ keys: [:tags], flag: '--tags', type: :boolean },
|
|
201
|
-
{ keys: [:contains], flag: '--contains', type: :boolean },
|
|
202
|
-
{ keys: [:debug], flag: '--debug', type: :boolean },
|
|
203
|
-
{ keys: [:long], flag: '--long', type: :boolean },
|
|
204
|
-
{ keys: [:always], flag: '--always', type: :boolean },
|
|
205
|
-
{ keys: %i[exact_match exact-match], flag: '--exact-match', type: :boolean },
|
|
206
|
-
{ keys: [:abbrev], flag: '--abbrev', type: :valued_equals },
|
|
207
|
-
{ keys: [:candidates], flag: '--candidates', type: :valued_equals },
|
|
208
|
-
{ keys: [:match], flag: '--match', type: :valued_equals },
|
|
209
|
-
{
|
|
210
|
-
keys: [:dirty],
|
|
211
|
-
type: :custom,
|
|
212
|
-
builder: lambda do |value|
|
|
213
|
-
return '--dirty' if value == true
|
|
214
|
-
|
|
215
|
-
"--dirty=#{value}" if value.is_a?(String)
|
|
216
|
-
end
|
|
217
|
-
}
|
|
218
|
-
].freeze
|
|
219
|
-
|
|
220
199
|
# Finds most recent tag that is reachable from a commit
|
|
221
200
|
#
|
|
222
201
|
# @see https://git-scm.com/docs/git-describe git-describe
|
|
@@ -225,13 +204,13 @@ module Git
|
|
|
225
204
|
#
|
|
226
205
|
# @param opts [Hash] the given options
|
|
227
206
|
#
|
|
228
|
-
# @option opts :all
|
|
229
|
-
# @option opts :tags
|
|
230
|
-
# @option opts :contains
|
|
231
|
-
# @option opts :debug
|
|
232
|
-
# @option opts :long
|
|
233
|
-
# @option opts :always
|
|
234
|
-
# @option opts :exact_match
|
|
207
|
+
# @option opts [Boolean, nil] :all (nil) use refs from all branches, tags, and remotes
|
|
208
|
+
# @option opts [Boolean, nil] :tags (nil) consider any tag, not just annotated ones
|
|
209
|
+
# @option opts [Boolean, nil] :contains (nil) find the tag that comes after the commit
|
|
210
|
+
# @option opts [Boolean, nil] :debug (nil) enable verbose searching strategy output
|
|
211
|
+
# @option opts [Boolean, nil] :long (nil) always output the long format
|
|
212
|
+
# @option opts [Boolean, nil] :always (nil) show uniquely abbreviated commit as a fallback
|
|
213
|
+
# @option opts [Boolean, nil] :exact_match (nil) only output exact tag matches
|
|
235
214
|
# @option opts :dirty [true, String]
|
|
236
215
|
# @option opts :abbrev [String]
|
|
237
216
|
# @option opts :candidates [String]
|
|
@@ -244,59 +223,14 @@ module Git
|
|
|
244
223
|
def describe(commit_ish = nil, opts = {})
|
|
245
224
|
assert_args_are_not_options('commit-ish object', commit_ish)
|
|
246
225
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
command('describe', *args)
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
# Return the commits that are within the given revision range
|
|
254
|
-
#
|
|
255
|
-
# @see https://git-scm.com/docs/git-log git-log
|
|
256
|
-
#
|
|
257
|
-
# @param opts [Hash] the given options
|
|
258
|
-
#
|
|
259
|
-
# @option opts :count [Integer] the maximum number of commits to return (maps to max-count)
|
|
260
|
-
# @option opts :all [Boolean]
|
|
261
|
-
# @option opts :cherry [Boolean]
|
|
262
|
-
# @option opts :since [String]
|
|
263
|
-
# @option opts :until [String]
|
|
264
|
-
# @option opts :grep [String]
|
|
265
|
-
# @option opts :author [String]
|
|
266
|
-
# @option opts :between [Array<String>] an array of two commit-ish strings to specify a revision range
|
|
267
|
-
#
|
|
268
|
-
# Only :between or :object options can be used, not both.
|
|
269
|
-
#
|
|
270
|
-
# @option opts :object [String] the revision range for the git log command
|
|
271
|
-
#
|
|
272
|
-
# Only :between or :object options can be used, not both.
|
|
273
|
-
#
|
|
274
|
-
# @option opts :path_limiter [String, Pathname, Array<String, Pathname>] only
|
|
275
|
-
# include commits that impact files from the specified paths
|
|
276
|
-
#
|
|
277
|
-
# @return [Array<String>] the log output
|
|
278
|
-
#
|
|
279
|
-
# @raise [ArgumentError] if the resulting revision range is a string starting with a hyphen
|
|
280
|
-
#
|
|
281
|
-
def log_commits(opts = {})
|
|
282
|
-
assert_args_are_not_options('between', opts[:between]&.first)
|
|
283
|
-
assert_args_are_not_options('object', opts[:object])
|
|
284
|
-
|
|
285
|
-
arr_opts = log_common_options(opts)
|
|
286
|
-
|
|
287
|
-
arr_opts << '--pretty=oneline'
|
|
288
|
-
|
|
289
|
-
arr_opts += log_path_options(opts)
|
|
226
|
+
# Translate legacy :"exact-match" (hyphenated) key to :exact_match (underscored)
|
|
227
|
+
opts = opts.dup
|
|
228
|
+
opts[:exact_match] ||= opts.delete(:'exact-match') if opts.key?(:'exact-match')
|
|
290
229
|
|
|
291
|
-
|
|
230
|
+
commit_ishes = Array(commit_ish).compact
|
|
231
|
+
Git::Commands::Describe.new(self).call(*commit_ishes, **opts).stdout
|
|
292
232
|
end
|
|
293
233
|
|
|
294
|
-
FULL_LOG_EXTRA_OPTIONS_MAP = [
|
|
295
|
-
{ type: :static, flag: '--pretty=raw' },
|
|
296
|
-
{ keys: [:skip], flag: '--skip', type: :valued_equals },
|
|
297
|
-
{ keys: [:merges], flag: '--merges', type: :boolean }
|
|
298
|
-
].freeze
|
|
299
|
-
|
|
300
234
|
# Return the commits that are within the given revision range
|
|
301
235
|
#
|
|
302
236
|
# @see https://git-scm.com/docs/git-log git-log
|
|
@@ -306,9 +240,9 @@ module Git
|
|
|
306
240
|
# @option opts :count [Integer] the maximum number of commits to return (maps to
|
|
307
241
|
# max-count)
|
|
308
242
|
#
|
|
309
|
-
# @option opts :all
|
|
243
|
+
# @option opts [Boolean, nil] :all (nil) include commits reachable from any ref
|
|
310
244
|
#
|
|
311
|
-
# @option opts :cherry
|
|
245
|
+
# @option opts [Boolean, nil] :cherry (nil) omit commits equivalent to cherry-picked commits
|
|
312
246
|
#
|
|
313
247
|
# @option opts :since [String]
|
|
314
248
|
#
|
|
@@ -352,17 +286,11 @@ module Git
|
|
|
352
286
|
# :object) is a string starting with a hyphen
|
|
353
287
|
#
|
|
354
288
|
def full_log_commits(opts = {})
|
|
355
|
-
|
|
356
|
-
|
|
289
|
+
assert_valid_opts(opts, FULL_LOG_ALLOWED_OPTS)
|
|
290
|
+
validate_log_count_option!(opts)
|
|
357
291
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
args += log_path_options(opts)
|
|
361
|
-
|
|
362
|
-
log_or_empty_on_unborn do
|
|
363
|
-
full_log = command_lines('log', *args)
|
|
364
|
-
process_commit_log_data(full_log)
|
|
365
|
-
end
|
|
292
|
+
call_opts = log_base_call_options(opts, skip: opts[:skip], merges: opts[:merges])
|
|
293
|
+
run_log_command(log_revision_range_args(opts), call_opts)
|
|
366
294
|
end
|
|
367
295
|
|
|
368
296
|
# Verify and resolve a Git revision to its full SHA
|
|
@@ -381,12 +309,9 @@ module Git
|
|
|
381
309
|
# @return [String] the full commit hash
|
|
382
310
|
#
|
|
383
311
|
# @raise [Git::FailedError] if the revision cannot be resolved
|
|
384
|
-
# @raise [ArgumentError] if the revision is a string starting with a hyphen
|
|
385
312
|
#
|
|
386
313
|
def rev_parse(revision)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
command('rev-parse', '--revs-only', '--end-of-options', revision, '--')
|
|
314
|
+
Git::Commands::RevParse.new(self).call(revision, '--', revs_only: true).stdout
|
|
390
315
|
end
|
|
391
316
|
|
|
392
317
|
# For backwards compatibility with the old method name
|
|
@@ -403,42 +328,69 @@ module Git
|
|
|
403
328
|
def name_rev(commit_ish)
|
|
404
329
|
assert_args_are_not_options('commit_ish', commit_ish)
|
|
405
330
|
|
|
406
|
-
|
|
331
|
+
Git::Commands::NameRev.new(self).call(commit_ish).stdout.split[1]
|
|
407
332
|
end
|
|
408
333
|
|
|
409
334
|
alias namerev name_rev
|
|
410
335
|
|
|
411
|
-
#
|
|
336
|
+
# Returns the raw content of a git object, or streams it into a tempfile
|
|
337
|
+
#
|
|
338
|
+
# Without a block, the full content is buffered in memory and returned as a
|
|
339
|
+
# `String`. With a block, git output is streamed directly to disk without memory
|
|
340
|
+
# buffering — safe for large blobs.
|
|
412
341
|
#
|
|
413
342
|
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
414
343
|
#
|
|
415
|
-
# @
|
|
416
|
-
#
|
|
344
|
+
# @overload cat_file_contents(object)
|
|
345
|
+
# Returns the object's raw content as a string.
|
|
417
346
|
#
|
|
418
|
-
#
|
|
419
|
-
# lib.cat_file_contents('README.md') { |f| f.read } # => "This is a README file\n"
|
|
347
|
+
# @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
|
|
420
348
|
#
|
|
421
|
-
#
|
|
349
|
+
# @return [String] the raw content of the object
|
|
422
350
|
#
|
|
423
|
-
#
|
|
351
|
+
# @raise [ArgumentError] if `object` starts with a hyphen
|
|
424
352
|
#
|
|
425
|
-
#
|
|
353
|
+
# @raise [Git::FailedError] if the object does not exist or the command fails
|
|
354
|
+
#
|
|
355
|
+
# @example Get the contents of a blob
|
|
356
|
+
# lib.cat_file_contents('HEAD:README.md') # => "This is a README file\n"
|
|
357
|
+
#
|
|
358
|
+
# @overload cat_file_contents(object, &block)
|
|
359
|
+
# Streams the object's raw content to a temporary file and yields it.
|
|
360
|
+
#
|
|
361
|
+
# Git output is written directly to a file on disk without being
|
|
362
|
+
# buffered in memory first, then the file is rewound and yielded to the block.
|
|
363
|
+
# The return value is whatever the block returns.
|
|
364
|
+
#
|
|
365
|
+
# @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
|
|
366
|
+
#
|
|
367
|
+
# @yield [file] the temporary file containing the streamed content, positioned at the start
|
|
368
|
+
#
|
|
369
|
+
# @yieldparam file [File] readable `IO` object positioned at the beginning of the content
|
|
370
|
+
#
|
|
371
|
+
# @yieldreturn [Object] the value to return from this method
|
|
372
|
+
#
|
|
373
|
+
# @return [Object] the value returned by the block
|
|
374
|
+
#
|
|
375
|
+
# @raise [ArgumentError] if `object` starts with a hyphen
|
|
376
|
+
#
|
|
377
|
+
# @raise [Git::FailedError] if the object does not exist or the command fails
|
|
378
|
+
#
|
|
379
|
+
# @example Read a large blob without buffering it in memory
|
|
380
|
+
# lib.cat_file_contents('HEAD:large_file.bin') { |f| process(f) }
|
|
426
381
|
#
|
|
427
382
|
def cat_file_contents(object)
|
|
428
383
|
assert_args_are_not_options('object', object)
|
|
429
384
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
else
|
|
440
|
-
# If a block is not given, return the file contents as a string
|
|
441
|
-
command('cat-file', '-p', object)
|
|
385
|
+
return Git::Commands::CatFile::Raw.new(self).call(object, p: true).stdout unless block_given?
|
|
386
|
+
|
|
387
|
+
# Stream git output directly to a tempfile to avoid buffering large
|
|
388
|
+
# object content in memory when a block is given.
|
|
389
|
+
Tempfile.create do |file|
|
|
390
|
+
file.binmode
|
|
391
|
+
Git::Commands::CatFile::Raw.new(self).call(object, p: true, out: file)
|
|
392
|
+
file.rewind
|
|
393
|
+
yield file
|
|
442
394
|
end
|
|
443
395
|
end
|
|
444
396
|
|
|
@@ -457,7 +409,7 @@ module Git
|
|
|
457
409
|
def cat_file_type(object)
|
|
458
410
|
assert_args_are_not_options('object', object)
|
|
459
411
|
|
|
460
|
-
|
|
412
|
+
cat_file_object_meta(object)[:type]
|
|
461
413
|
end
|
|
462
414
|
|
|
463
415
|
alias object_type cat_file_type
|
|
@@ -466,16 +418,16 @@ module Git
|
|
|
466
418
|
#
|
|
467
419
|
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
468
420
|
#
|
|
469
|
-
# @param object [String] the object to get the
|
|
421
|
+
# @param object [String] the object to get the size of
|
|
470
422
|
#
|
|
471
|
-
# @return [
|
|
423
|
+
# @return [Integer] the object size in bytes
|
|
472
424
|
#
|
|
473
425
|
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
474
426
|
#
|
|
475
427
|
def cat_file_size(object)
|
|
476
428
|
assert_args_are_not_options('object', object)
|
|
477
429
|
|
|
478
|
-
|
|
430
|
+
cat_file_object_meta(object)[:size]
|
|
479
431
|
end
|
|
480
432
|
|
|
481
433
|
alias object_size cat_file_size
|
|
@@ -501,7 +453,7 @@ module Git
|
|
|
501
453
|
def cat_file_commit(object)
|
|
502
454
|
assert_args_are_not_options('object', object)
|
|
503
455
|
|
|
504
|
-
cdata =
|
|
456
|
+
cdata = Git::Commands::CatFile::Raw.new(self).call('commit', object).stdout.split("\n")
|
|
505
457
|
process_commit_data(cdata, object)
|
|
506
458
|
end
|
|
507
459
|
|
|
@@ -518,6 +470,26 @@ module Git
|
|
|
518
470
|
|
|
519
471
|
CAT_FILE_HEADER_LINE = /\A(?<key>\w+) (?<value>.*)\z/
|
|
520
472
|
|
|
473
|
+
# Yields parsed header key/value pairs from `git cat-file` output lines
|
|
474
|
+
#
|
|
475
|
+
# Consumes header lines from the front of `data` until a non-header line is
|
|
476
|
+
# encountered. Continuation lines that begin with a space are folded into the
|
|
477
|
+
# previous header value using newline separators.
|
|
478
|
+
#
|
|
479
|
+
# @param data [Array<String>] mutable output lines from a cat-file response
|
|
480
|
+
#
|
|
481
|
+
# @yield [key, value] each parsed header pair
|
|
482
|
+
#
|
|
483
|
+
# @yieldparam key [String] header field name
|
|
484
|
+
#
|
|
485
|
+
# @yieldparam value [String] unfolded header value text
|
|
486
|
+
#
|
|
487
|
+
# @yieldreturn [void]
|
|
488
|
+
#
|
|
489
|
+
# @return [void]
|
|
490
|
+
#
|
|
491
|
+
# @raise [NoMethodError] if `data` contains non-string entries
|
|
492
|
+
#
|
|
521
493
|
def each_cat_file_header(data)
|
|
522
494
|
while (match = CAT_FILE_HEADER_LINE.match(data.shift))
|
|
523
495
|
key = match[:key]
|
|
@@ -570,12 +542,39 @@ module Git
|
|
|
570
542
|
def cat_file_tag(object)
|
|
571
543
|
assert_args_are_not_options('object', object)
|
|
572
544
|
|
|
573
|
-
tdata =
|
|
545
|
+
tdata = Git::Commands::CatFile::Raw.new(self).call('tag', object).stdout.split("\n")
|
|
574
546
|
process_tag_data(tdata, object)
|
|
575
547
|
end
|
|
576
548
|
|
|
577
549
|
alias tag_data cat_file_tag
|
|
578
550
|
|
|
551
|
+
def cat_file_object_meta(object)
|
|
552
|
+
stdout = Git::Commands::CatFile::Batch.new(self).call(object, batch_check: true).stdout
|
|
553
|
+
parse_cat_file_meta(stdout, object)
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def parse_cat_file_meta(output, object)
|
|
557
|
+
line = output.to_s.lines.first.to_s.chomp
|
|
558
|
+
|
|
559
|
+
request_object_to_raise_error!(object) if line == "#{object} missing"
|
|
560
|
+
|
|
561
|
+
match = /\A\S+ (?<type>\S+) (?<size>\d+)\z/.match(line)
|
|
562
|
+
raise Git::UnexpectedResultError, "unexpected git cat-file metadata output: #{line.inspect}" if match.nil?
|
|
563
|
+
|
|
564
|
+
{
|
|
565
|
+
type: match[:type],
|
|
566
|
+
size: match[:size].to_i
|
|
567
|
+
}
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Re-request the missing object via non-batch cat-file so git produces a
|
|
571
|
+
# real non-zero exit and a FailedError with an accurate stderr message.
|
|
572
|
+
def request_object_to_raise_error!(object)
|
|
573
|
+
Git::Commands::CatFile::Raw.new(self).call(object, p: true)
|
|
574
|
+
raise Git::UnexpectedResultError,
|
|
575
|
+
"expected git cat-file to raise Git::FailedError for missing object #{object.inspect}"
|
|
576
|
+
end
|
|
577
|
+
|
|
579
578
|
def process_tag_data(data, name)
|
|
580
579
|
hsh = { 'name' => name }
|
|
581
580
|
|
|
@@ -650,32 +649,48 @@ module Git
|
|
|
650
649
|
end
|
|
651
650
|
private_constant :RawLogParser
|
|
652
651
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
].freeze
|
|
652
|
+
# Allowed option keys for {#ls_tree}
|
|
653
|
+
LS_TREE_ALLOWED_OPTS = %i[recursive path].freeze
|
|
656
654
|
|
|
655
|
+
# Lists the objects in a git tree
|
|
656
|
+
#
|
|
657
|
+
# @param sha [String] the tree-ish object to list
|
|
658
|
+
#
|
|
659
|
+
# @param opts [Hash] additional options
|
|
660
|
+
#
|
|
661
|
+
# @return [Hash<String, Hash<String, Hash>>] parsed ls-tree output
|
|
662
|
+
#
|
|
663
|
+
# @api private
|
|
664
|
+
#
|
|
657
665
|
def ls_tree(sha, opts = {})
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
666
|
+
assert_valid_opts(opts, LS_TREE_ALLOWED_OPTS)
|
|
667
|
+
r_value = opts[:recursive]
|
|
668
|
+
paths = Array(opts[:path]).compact
|
|
669
|
+
safe_options = {}
|
|
670
|
+
safe_options[:r] = r_value unless r_value.nil?
|
|
671
|
+
result = Git::Commands::LsTree.new(self).call(sha, *paths, **safe_options)
|
|
672
|
+
parse_ls_tree_output(result.stdout)
|
|
673
|
+
end
|
|
663
674
|
|
|
664
|
-
|
|
675
|
+
def parse_ls_tree_output(output)
|
|
676
|
+
data = { 'blob' => {}, 'tree' => {}, 'commit' => {} }
|
|
677
|
+
output.split("\n").each do |line|
|
|
665
678
|
(info, filenm) = split_status_line(line)
|
|
666
|
-
(mode, type,
|
|
667
|
-
data[type][filenm] = { mode: mode, sha:
|
|
679
|
+
(mode, type, entry_sha) = info.split
|
|
680
|
+
data[type][filenm] = { mode: mode, sha: entry_sha }
|
|
668
681
|
end
|
|
669
|
-
|
|
670
682
|
data
|
|
671
683
|
end
|
|
684
|
+
private :parse_ls_tree_output
|
|
672
685
|
|
|
673
|
-
|
|
674
|
-
|
|
686
|
+
# @return [String] the command output
|
|
687
|
+
#
|
|
688
|
+
def mv(source, destination, options = {})
|
|
689
|
+
Git::Commands::Mv.new(self).call(*Array(source), destination, verbose: true, **options).stdout
|
|
675
690
|
end
|
|
676
691
|
|
|
677
692
|
def full_tree(sha)
|
|
678
|
-
|
|
693
|
+
Git::Commands::LsTree.new(self).call(sha, r: true).stdout.split("\n")
|
|
679
694
|
end
|
|
680
695
|
|
|
681
696
|
def tree_depth(sha)
|
|
@@ -683,38 +698,12 @@ module Git
|
|
|
683
698
|
end
|
|
684
699
|
|
|
685
700
|
def change_head_branch(branch_name)
|
|
686
|
-
|
|
701
|
+
Git::Commands::SymbolicRef::Update.new(self).call('HEAD', "refs/heads/#{branch_name}")
|
|
687
702
|
end
|
|
688
703
|
|
|
689
|
-
BRANCH_LINE_REGEXP = /
|
|
690
|
-
^
|
|
691
|
-
# Prefix indicates if this branch is checked out. The prefix is one of:
|
|
692
|
-
(?:
|
|
693
|
-
(?<current>\*[[:blank:]]) | # Current branch (checked out in the current worktree)
|
|
694
|
-
(?<worktree>\+[[:blank:]]) | # Branch checked out in a different worktree
|
|
695
|
-
[[:blank:]]{2} # Branch not checked out
|
|
696
|
-
)
|
|
697
|
-
|
|
698
|
-
# The branch's full refname
|
|
699
|
-
(?:
|
|
700
|
-
(?<not_a_branch>\(not[[:blank:]]a[[:blank:]]branch\)) |
|
|
701
|
-
(?:\(HEAD[[:blank:]]detached[[:blank:]]at[[:blank:]](?<detached_ref>[^)]+)\)) |
|
|
702
|
-
(?<refname>[^[[:blank:]]]+)
|
|
703
|
-
)
|
|
704
|
-
|
|
705
|
-
# Optional symref
|
|
706
|
-
# If this ref is a symbolic reference, this is the ref referenced
|
|
707
|
-
(?:
|
|
708
|
-
[[:blank:]]->[[:blank:]](?<symref>.*)
|
|
709
|
-
)?
|
|
710
|
-
$
|
|
711
|
-
/x
|
|
712
|
-
|
|
713
704
|
def branches_all
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
parse_branch_line(line, index, lines)
|
|
717
|
-
end
|
|
705
|
+
result = Git::Commands::Branch::List.new(self).call(all: true, format: Git::Parsers::Branch::FORMAT_STRING)
|
|
706
|
+
Git::Parsers::Branch.parse_list(result.stdout)
|
|
718
707
|
end
|
|
719
708
|
|
|
720
709
|
def worktrees_all
|
|
@@ -729,7 +718,7 @@ module Git
|
|
|
729
718
|
# HEAD b8c63206f8d10f57892060375a86ae911fad356e
|
|
730
719
|
# detached
|
|
731
720
|
#
|
|
732
|
-
|
|
721
|
+
Git::Commands::Worktree::List.new(self).call(porcelain: true).stdout.split("\n").each do |w|
|
|
733
722
|
s = w.split
|
|
734
723
|
directory = s[1] if s[0] == 'worktree'
|
|
735
724
|
arr << [directory, s[1]] if s[0] == 'HEAD'
|
|
@@ -738,17 +727,19 @@ module Git
|
|
|
738
727
|
end
|
|
739
728
|
|
|
740
729
|
def worktree_add(dir, commitish = nil)
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
730
|
+
if commitish.nil?
|
|
731
|
+
Git::Commands::Worktree::Add.new(self).call(dir).stdout
|
|
732
|
+
else
|
|
733
|
+
Git::Commands::Worktree::Add.new(self).call(dir, commitish).stdout
|
|
734
|
+
end
|
|
744
735
|
end
|
|
745
736
|
|
|
746
737
|
def worktree_remove(dir)
|
|
747
|
-
|
|
738
|
+
Git::Commands::Worktree::Remove.new(self).call(dir).stdout
|
|
748
739
|
end
|
|
749
740
|
|
|
750
741
|
def worktree_prune
|
|
751
|
-
|
|
742
|
+
Git::Commands::Worktree::Prune.new(self).call.stdout
|
|
752
743
|
end
|
|
753
744
|
|
|
754
745
|
def list_files(ref_dir)
|
|
@@ -785,7 +776,7 @@ module Git
|
|
|
785
776
|
# @return [HeadState] the state and name of the current branch
|
|
786
777
|
#
|
|
787
778
|
def current_branch_state
|
|
788
|
-
branch_name =
|
|
779
|
+
branch_name = Git::Commands::Branch::ShowCurrent.new(self).call.stdout
|
|
789
780
|
return HeadState.new(:detached, 'HEAD') if branch_name.empty?
|
|
790
781
|
|
|
791
782
|
state = get_branch_state(branch_name)
|
|
@@ -793,39 +784,36 @@ module Git
|
|
|
793
784
|
end
|
|
794
785
|
|
|
795
786
|
def branch_current
|
|
796
|
-
|
|
797
|
-
|
|
787
|
+
result = Git::Commands::Branch::ShowCurrent.new(self).call
|
|
788
|
+
name = result.stdout.strip
|
|
789
|
+
name.empty? ? 'HEAD' : name
|
|
798
790
|
end
|
|
799
791
|
|
|
800
792
|
def branch_contains(commit, branch_name = '')
|
|
801
|
-
|
|
793
|
+
branch_name = branch_name.to_s
|
|
794
|
+
pattern = branch_name.empty? ? nil : branch_name
|
|
795
|
+
Git::Commands::Branch::List.new(self).call(*[pattern].compact, contains: commit, no_color: true).stdout
|
|
802
796
|
end
|
|
803
797
|
|
|
804
|
-
|
|
805
|
-
{ keys: [:ignore_case], flag: '-i', type: :boolean },
|
|
806
|
-
{ keys: [:invert_match], flag: '-v', type: :boolean },
|
|
807
|
-
{ keys: [:extended_regexp], flag: '-E', type: :boolean },
|
|
808
|
-
# For validation only, as these are handled manually
|
|
809
|
-
{ keys: [:object], type: :validate_only },
|
|
810
|
-
{ keys: [:path_limiter], type: :validate_only }
|
|
811
|
-
].freeze
|
|
798
|
+
GREP_ALLOWED_OPTS = %i[ignore_case i invert_match v extended_regexp E object path_limiter].freeze
|
|
812
799
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
# [tree-ish] = [[line_no, match], [line_no, match2]]
|
|
816
|
-
def grep(string, opts = {})
|
|
817
|
-
opts[:object] ||= 'HEAD'
|
|
818
|
-
ArgsBuilder.validate!(opts, GREP_OPTION_MAP)
|
|
800
|
+
def grep(pattern, opts = {})
|
|
801
|
+
assert_valid_opts(opts, GREP_ALLOWED_OPTS)
|
|
819
802
|
|
|
820
|
-
|
|
821
|
-
|
|
803
|
+
opts = normalize_grep_opts(opts)
|
|
804
|
+
object = opts.delete(:object) || 'HEAD'
|
|
805
|
+
result = Git::Commands::Grep.new(self).call(
|
|
806
|
+
object, pattern:, **opts, no_color: true, line_number: true, null: true
|
|
807
|
+
)
|
|
808
|
+
exitstatus = result.status.exitstatus
|
|
822
809
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
810
|
+
# Exit status 1 with empty stderr means no lines matched (not an error)
|
|
811
|
+
return {} if exitstatus == 1 && result.stderr.empty?
|
|
812
|
+
|
|
813
|
+
# Exit status 1 with non-empty stderr is a real error (e.g. bad object reference)
|
|
814
|
+
raise Git::FailedError, result if exitstatus == 1
|
|
826
815
|
|
|
827
|
-
|
|
828
|
-
parse_grep_output(lines)
|
|
816
|
+
parse_grep_output(result.stdout)
|
|
829
817
|
end
|
|
830
818
|
|
|
831
819
|
# Validate that the given arguments cannot be mistaken for a command-line option
|
|
@@ -878,7 +866,24 @@ module Git
|
|
|
878
866
|
raise ArgumentError, "Invalid #{arg_name}: must be a String, Pathname, or Array of Strings/Pathnames"
|
|
879
867
|
end
|
|
880
868
|
|
|
869
|
+
# Allowed option keys for {#full_log_commits}
|
|
870
|
+
FULL_LOG_ALLOWED_OPTS = %i[count all cherry since until grep author between object path_limiter skip merges].freeze
|
|
871
|
+
|
|
872
|
+
# Allowed option keys for {#diff_full}
|
|
873
|
+
DIFF_FULL_ALLOWED_OPTS = %i[path_limiter].freeze
|
|
874
|
+
|
|
875
|
+
# Allowed option keys for {#diff_stats}
|
|
876
|
+
DIFF_STATS_ALLOWED_OPTS = %i[path_limiter].freeze
|
|
877
|
+
|
|
878
|
+
# Allowed option keys for {#diff_path_status}
|
|
879
|
+
DIFF_PATH_STATUS_ALLOWED_OPTS = %i[path_limiter path].freeze
|
|
880
|
+
|
|
881
881
|
# Handle deprecated :path option in favor of :path_limiter
|
|
882
|
+
#
|
|
883
|
+
# @param opts [Hash] options hash that may contain :path or :path_limiter
|
|
884
|
+
#
|
|
885
|
+
# @return [String, Pathname, Array<String, Pathname>, nil] the resolved path limiter
|
|
886
|
+
#
|
|
882
887
|
def handle_deprecated_path_option(opts)
|
|
883
888
|
if opts.key?(:path_limiter)
|
|
884
889
|
opts[:path_limiter]
|
|
@@ -890,74 +895,131 @@ module Git
|
|
|
890
895
|
end
|
|
891
896
|
end
|
|
892
897
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
898
|
+
# Validate that opts contains only allowed keys
|
|
899
|
+
#
|
|
900
|
+
# @param opts [Hash] options hash to validate
|
|
901
|
+
#
|
|
902
|
+
# @param allowed [Array<Symbol>] allowed option keys
|
|
903
|
+
#
|
|
904
|
+
# @raise [ArgumentError] if unknown keys are present
|
|
905
|
+
#
|
|
906
|
+
def assert_valid_opts(opts, allowed)
|
|
907
|
+
unknown = opts.keys - allowed
|
|
908
|
+
raise ArgumentError, "Unknown options: #{unknown.join(', ')}" if unknown.any?
|
|
909
|
+
end
|
|
897
910
|
|
|
911
|
+
# Show full diff patch output between commits or the working tree
|
|
912
|
+
#
|
|
913
|
+
# Delegates to {Git::Commands::Diff}.
|
|
914
|
+
#
|
|
915
|
+
# @param obj1 [String] first commit reference (default: 'HEAD')
|
|
916
|
+
#
|
|
917
|
+
# @param obj2 [String, nil] second commit reference (default: nil)
|
|
918
|
+
#
|
|
919
|
+
# @param opts [Hash] options
|
|
920
|
+
#
|
|
921
|
+
# @option opts [String, Pathname, Array<String, Pathname>] :path_limiter (nil)
|
|
922
|
+
# pathspecs to limit the diff
|
|
923
|
+
#
|
|
924
|
+
# @return [String] the unified diff patch output
|
|
925
|
+
#
|
|
926
|
+
# @raise [Git::FailedError] if git returns exit code > 2
|
|
927
|
+
#
|
|
928
|
+
# @see Git::Commands::Diff
|
|
929
|
+
#
|
|
898
930
|
def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
command('diff', *args)
|
|
931
|
+
assert_valid_opts(opts, DIFF_FULL_ALLOWED_OPTS)
|
|
932
|
+
pathspecs = normalize_pathspecs(opts[:path_limiter], 'path limiter')
|
|
933
|
+
result = Git::Commands::Diff.new(self).call(
|
|
934
|
+
*[obj1, obj2].compact,
|
|
935
|
+
patch: true, numstat: true, shortstat: true,
|
|
936
|
+
src_prefix: 'a/', dst_prefix: 'b/',
|
|
937
|
+
path: pathspecs
|
|
938
|
+
)
|
|
939
|
+
extract_patch_text(result.stdout)
|
|
910
940
|
end
|
|
911
941
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
942
|
+
# Show numstat diff output between commits or the working tree
|
|
943
|
+
#
|
|
944
|
+
# Delegates to {Git::Commands::Diff}.
|
|
945
|
+
#
|
|
946
|
+
# @param obj1 [String] first commit reference (default: 'HEAD')
|
|
947
|
+
#
|
|
948
|
+
# @param obj2 [String, nil] second commit reference (default: nil)
|
|
949
|
+
#
|
|
950
|
+
# @param opts [Hash] options
|
|
951
|
+
#
|
|
952
|
+
# @option opts [String, Pathname, Array<String, Pathname>] :path_limiter (nil)
|
|
953
|
+
# pathspecs to limit the diff
|
|
954
|
+
#
|
|
955
|
+
# @return [Hash] diff statistics with the shape:
|
|
956
|
+
# `{ total: { insertions:, deletions:, lines:, files: }, files: { ... } }`
|
|
957
|
+
#
|
|
958
|
+
# @raise [Git::FailedError] if git returns exit code > 2
|
|
959
|
+
#
|
|
960
|
+
# @see Git::Commands::Diff
|
|
961
|
+
#
|
|
917
962
|
def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
output_lines = command_lines('diff', *args)
|
|
963
|
+
assert_valid_opts(opts, DIFF_STATS_ALLOWED_OPTS)
|
|
964
|
+
pathspecs = normalize_pathspecs(opts[:path_limiter], 'path limiter')
|
|
965
|
+
result = Git::Commands::Diff.new(self).call(
|
|
966
|
+
*[obj1, obj2].compact,
|
|
967
|
+
numstat: true, shortstat: true,
|
|
968
|
+
src_prefix: 'a/', dst_prefix: 'b/',
|
|
969
|
+
path: pathspecs
|
|
970
|
+
)
|
|
971
|
+
output_lines = extract_numstat_lines(result.stdout)
|
|
929
972
|
parse_diff_stats_output(output_lines)
|
|
930
973
|
end
|
|
931
974
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
975
|
+
# Show path status (name-status) for diff between commits or the working tree
|
|
976
|
+
#
|
|
977
|
+
# Delegates to {Git::Commands::Diff} and extracts status letters and
|
|
978
|
+
# paths from the raw output lines.
|
|
979
|
+
#
|
|
980
|
+
# @param reference1 [String, nil] first commit reference (default: nil)
|
|
981
|
+
#
|
|
982
|
+
# @param reference2 [String, nil] second commit reference (default: nil)
|
|
983
|
+
#
|
|
984
|
+
# @param opts [Hash] options
|
|
985
|
+
#
|
|
986
|
+
# @option opts [String, Pathname, Array<String, Pathname>] :path_limiter (nil)
|
|
987
|
+
# pathspecs to limit the diff
|
|
988
|
+
#
|
|
989
|
+
# @option opts [String, Pathname, Array<String, Pathname>] :path (nil)
|
|
990
|
+
# deprecated; use :path_limiter instead
|
|
991
|
+
#
|
|
992
|
+
# @return [Hash] mapping of file paths to status letters
|
|
993
|
+
# (e.g. `{ "lib/foo.rb" => "M", "README.md" => "A" }`)
|
|
994
|
+
#
|
|
995
|
+
# @raise [Git::FailedError] if git returns exit code > 2
|
|
996
|
+
#
|
|
997
|
+
# @see Git::Commands::Diff
|
|
998
|
+
#
|
|
938
999
|
def diff_path_status(reference1 = nil, reference2 = nil, opts = {})
|
|
939
|
-
|
|
940
|
-
ArgsBuilder.validate!(opts, DIFF_PATH_STATUS_OPTION_MAP)
|
|
941
|
-
|
|
942
|
-
args = build_args(opts, DIFF_PATH_STATUS_OPTION_MAP)
|
|
943
|
-
args.push(reference1, reference2).compact!
|
|
1000
|
+
assert_valid_opts(opts, DIFF_PATH_STATUS_ALLOWED_OPTS)
|
|
944
1001
|
|
|
945
1002
|
path_limiter = handle_deprecated_path_option(opts)
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1003
|
+
pathspecs = normalize_pathspecs(path_limiter, 'path limiter')
|
|
1004
|
+
result = Git::Commands::Diff.new(self).call(
|
|
1005
|
+
*[reference1, reference2].compact,
|
|
1006
|
+
raw: true, numstat: true, shortstat: true,
|
|
1007
|
+
src_prefix: 'a/', dst_prefix: 'b/',
|
|
1008
|
+
path: pathspecs
|
|
1009
|
+
)
|
|
1010
|
+
extract_name_status_from_raw(result.stdout)
|
|
951
1011
|
end
|
|
952
1012
|
|
|
953
1013
|
# compares the index and the working directory
|
|
954
1014
|
def diff_files
|
|
955
|
-
|
|
1015
|
+
Git::Commands::Status.new(self).call
|
|
1016
|
+
parse_raw_diff_output(Git::Commands::DiffFiles.new(self).call.stdout)
|
|
956
1017
|
end
|
|
957
1018
|
|
|
958
1019
|
# compares the index and the repository
|
|
959
1020
|
def diff_index(treeish)
|
|
960
|
-
|
|
1021
|
+
Git::Commands::Status.new(self).call
|
|
1022
|
+
parse_raw_diff_output(Git::Commands::DiffIndex.new(self).call(treeish).stdout)
|
|
961
1023
|
end
|
|
962
1024
|
|
|
963
1025
|
# List all files that are in the index
|
|
@@ -975,7 +1037,7 @@ module Git
|
|
|
975
1037
|
def ls_files(location = nil)
|
|
976
1038
|
location ||= '.'
|
|
977
1039
|
{}.tap do |files|
|
|
978
|
-
|
|
1040
|
+
Git::Commands::LsFiles.new(self).call(location, stage: true).stdout.split("\n").each do |line|
|
|
979
1041
|
(info, file) = split_status_line(line)
|
|
980
1042
|
(mode, sha, stage) = info.split
|
|
981
1043
|
files[file] = {
|
|
@@ -1008,28 +1070,22 @@ module Git
|
|
|
1008
1070
|
end
|
|
1009
1071
|
end
|
|
1010
1072
|
|
|
1011
|
-
LS_REMOTE_OPTION_MAP = [
|
|
1012
|
-
{ keys: [:refs], flag: '--refs', type: :boolean }
|
|
1013
|
-
].freeze
|
|
1014
|
-
|
|
1015
1073
|
def ls_remote(location = nil, opts = {})
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
flags = build_args(opts, LS_REMOTE_OPTION_MAP)
|
|
1019
|
-
positional_arg = location || '.'
|
|
1020
|
-
|
|
1021
|
-
output_lines = command_lines('ls-remote', *flags, positional_arg)
|
|
1074
|
+
repository = location || '.'
|
|
1075
|
+
output_lines = Git::Commands::LsRemote.new(self).call(repository, **opts).stdout.split("\n")
|
|
1022
1076
|
parse_ls_remote_output(output_lines)
|
|
1023
1077
|
end
|
|
1024
1078
|
|
|
1025
1079
|
def ignored_files
|
|
1026
|
-
|
|
1080
|
+
Git::Commands::LsFiles.new(self).call(
|
|
1081
|
+
others: true, ignored: true, exclude_standard: true
|
|
1082
|
+
).stdout.split("\n").map { |f| unescape_quoted_path(f) }
|
|
1027
1083
|
end
|
|
1028
1084
|
|
|
1029
1085
|
def untracked_files
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1086
|
+
Git::Commands::LsFiles.new(self).call(
|
|
1087
|
+
others: true, exclude_standard: true, chdir: @git_work_dir
|
|
1088
|
+
).stdout.split("\n").map { |f| unescape_quoted_path(f) }
|
|
1033
1089
|
end
|
|
1034
1090
|
|
|
1035
1091
|
def config_remote(name)
|
|
@@ -1041,19 +1097,25 @@ module Git
|
|
|
1041
1097
|
end
|
|
1042
1098
|
|
|
1043
1099
|
def config_get(name)
|
|
1044
|
-
|
|
1100
|
+
result = Git::Commands::ConfigOptionSyntax::Get.new(self).call(name)
|
|
1101
|
+
raise Git::FailedError, result if result.status.exitstatus != 0
|
|
1102
|
+
|
|
1103
|
+
result.stdout
|
|
1045
1104
|
end
|
|
1046
1105
|
|
|
1047
1106
|
def global_config_get(name)
|
|
1048
|
-
|
|
1107
|
+
result = Git::Commands::ConfigOptionSyntax::Get.new(self).call(name, global: true)
|
|
1108
|
+
raise Git::FailedError, result if result.status.exitstatus != 0
|
|
1109
|
+
|
|
1110
|
+
result.stdout
|
|
1049
1111
|
end
|
|
1050
1112
|
|
|
1051
1113
|
def config_list
|
|
1052
|
-
parse_config_list
|
|
1114
|
+
parse_config_list Git::Commands::ConfigOptionSyntax::List.new(self).call.stdout.split("\n")
|
|
1053
1115
|
end
|
|
1054
1116
|
|
|
1055
1117
|
def global_config_list
|
|
1056
|
-
parse_config_list
|
|
1118
|
+
parse_config_list Git::Commands::ConfigOptionSyntax::List.new(self).call(global: true).stdout.split("\n")
|
|
1057
1119
|
end
|
|
1058
1120
|
|
|
1059
1121
|
def parse_config_list(lines)
|
|
@@ -1066,7 +1128,7 @@ module Git
|
|
|
1066
1128
|
end
|
|
1067
1129
|
|
|
1068
1130
|
def parse_config(file)
|
|
1069
|
-
parse_config_list
|
|
1131
|
+
parse_config_list Git::Commands::ConfigOptionSyntax::List.new(self).call(file: file).stdout.split("\n")
|
|
1070
1132
|
end
|
|
1071
1133
|
|
|
1072
1134
|
# Shows objects
|
|
@@ -1075,34 +1137,23 @@ module Git
|
|
|
1075
1137
|
# @param [String|NilClass] path the path of the file to be shown
|
|
1076
1138
|
# @return [String] the object information
|
|
1077
1139
|
def show(objectish = nil, path = nil)
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
arr_opts << (path ? "#{objectish}:#{path}" : objectish)
|
|
1081
|
-
|
|
1082
|
-
command('show', *arr_opts.compact, chomp: false)
|
|
1140
|
+
object = path ? "#{objectish}:#{path}" : objectish
|
|
1141
|
+
Git::Commands::Show.new(self).call(*[object].compact).stdout
|
|
1083
1142
|
end
|
|
1084
1143
|
|
|
1085
1144
|
## WRITE COMMANDS ##
|
|
1086
1145
|
|
|
1087
|
-
|
|
1088
|
-
{ keys: [:file], flag: '--file', type: :valued_space }
|
|
1089
|
-
].freeze
|
|
1146
|
+
CONFIG_SET_ALLOWED_OPTS = %i[file].freeze
|
|
1090
1147
|
|
|
1091
1148
|
def config_set(name, value, options = {})
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
command('config', *flags, name, value)
|
|
1149
|
+
assert_valid_opts(options, CONFIG_SET_ALLOWED_OPTS)
|
|
1150
|
+
Git::Commands::ConfigOptionSyntax::Set.new(self).call(name, value, **options.slice(*CONFIG_SET_ALLOWED_OPTS))
|
|
1095
1151
|
end
|
|
1096
1152
|
|
|
1097
1153
|
def global_config_set(name, value)
|
|
1098
|
-
|
|
1154
|
+
Git::Commands::ConfigOptionSyntax::Set.new(self).call(name, value, global: true)
|
|
1099
1155
|
end
|
|
1100
1156
|
|
|
1101
|
-
ADD_OPTION_MAP = [
|
|
1102
|
-
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1103
|
-
{ keys: [:force], flag: '--force', type: :boolean }
|
|
1104
|
-
].freeze
|
|
1105
|
-
|
|
1106
1157
|
# Update the index from the current worktree to prepare the for the next commit
|
|
1107
1158
|
#
|
|
1108
1159
|
# @example
|
|
@@ -1113,31 +1164,28 @@ module Git
|
|
|
1113
1164
|
# @param [String, Array<String>] paths files to be added to the repository (relative to the worktree root)
|
|
1114
1165
|
# @param [Hash] options
|
|
1115
1166
|
#
|
|
1116
|
-
# @option options [Boolean] :all
|
|
1117
|
-
# @option options [Boolean] :force
|
|
1167
|
+
# @option options [Boolean, nil] :all (nil) add, modify, and remove index entries to match the worktree
|
|
1168
|
+
# @option options [Boolean, nil] :force (nil) allow adding otherwise ignored files
|
|
1169
|
+
#
|
|
1170
|
+
# @return [String] the command output (typically empty on success)
|
|
1118
1171
|
#
|
|
1119
1172
|
def add(paths = '.', options = {})
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
args << '--'
|
|
1123
|
-
args.concat(Array(paths))
|
|
1124
|
-
|
|
1125
|
-
command('add', *args)
|
|
1173
|
+
Git::Commands::Add.new(self).call(*Array(paths), **options).stdout
|
|
1126
1174
|
end
|
|
1127
1175
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1176
|
+
# Remove files from the working tree and from the index
|
|
1177
|
+
#
|
|
1178
|
+
# @param path [String, Array<String>] files or directories to remove
|
|
1179
|
+
# @param opts [Hash] command options
|
|
1180
|
+
#
|
|
1181
|
+
# @option opts [Boolean, nil] :force (nil) force removal, bypassing the up-to-date check; alias: `:f`
|
|
1182
|
+
# @option opts [Boolean, nil] :recursive (nil) remove directories and their contents recursively
|
|
1183
|
+
# @option opts [Boolean, nil] :cached (nil) only remove from the index, keeping working tree files
|
|
1184
|
+
#
|
|
1185
|
+
# @return [String] the command output
|
|
1186
|
+
#
|
|
1134
1187
|
def rm(path = '.', opts = {})
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
args << '--'
|
|
1138
|
-
args.concat(Array(path))
|
|
1139
|
-
|
|
1140
|
-
command('rm', *args)
|
|
1188
|
+
Git::Commands::Rm.new(self).call(*Array(path), **opts).stdout
|
|
1141
1189
|
end
|
|
1142
1190
|
|
|
1143
1191
|
# Returns true if the repository is empty (meaning it has no commits)
|
|
@@ -1145,7 +1193,7 @@ module Git
|
|
|
1145
1193
|
# @return [Boolean]
|
|
1146
1194
|
#
|
|
1147
1195
|
def empty?
|
|
1148
|
-
|
|
1196
|
+
Git::Commands::RevParse.new(self).call('HEAD', verify: true)
|
|
1149
1197
|
false
|
|
1150
1198
|
rescue Git::FailedError => e
|
|
1151
1199
|
raise unless e.result.status.exitstatus == 128 &&
|
|
@@ -1154,27 +1202,6 @@ module Git
|
|
|
1154
1202
|
true
|
|
1155
1203
|
end
|
|
1156
1204
|
|
|
1157
|
-
COMMIT_OPTION_MAP = [
|
|
1158
|
-
{ keys: %i[add_all all], flag: '--all', type: :boolean },
|
|
1159
|
-
{ keys: [:allow_empty], flag: '--allow-empty', type: :boolean },
|
|
1160
|
-
{ keys: [:no_verify], flag: '--no-verify', type: :boolean },
|
|
1161
|
-
{ keys: [:allow_empty_message], flag: '--allow-empty-message', type: :boolean },
|
|
1162
|
-
{ keys: [:author], flag: '--author', type: :valued_equals },
|
|
1163
|
-
{ keys: [:message], flag: '--message', type: :valued_equals },
|
|
1164
|
-
{ keys: [:no_gpg_sign], flag: '--no-gpg-sign', type: :boolean },
|
|
1165
|
-
{ keys: [:date], flag: '--date', type: :valued_equals, validator: ->(v) { v.is_a?(String) } },
|
|
1166
|
-
{ keys: [:amend], type: :custom, builder: ->(value) { ['--amend', '--no-edit'] if value } },
|
|
1167
|
-
{
|
|
1168
|
-
keys: [:gpg_sign],
|
|
1169
|
-
type: :custom,
|
|
1170
|
-
builder: lambda { |value|
|
|
1171
|
-
if value
|
|
1172
|
-
value == true ? '--gpg-sign' : "--gpg-sign=#{value}"
|
|
1173
|
-
end
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
].freeze
|
|
1177
|
-
|
|
1178
1205
|
# Takes the commit message with the options and executes the commit command
|
|
1179
1206
|
#
|
|
1180
1207
|
# accepts options:
|
|
@@ -1190,176 +1217,284 @@ module Git
|
|
|
1190
1217
|
#
|
|
1191
1218
|
# @param [String] message the commit message to be used
|
|
1192
1219
|
# @param [Hash] opts the commit options to be used
|
|
1193
|
-
|
|
1220
|
+
#
|
|
1194
1221
|
def commit(message, opts = {})
|
|
1195
|
-
opts
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
raise ArgumentError, 'cannot specify :gpg_sign and :no_gpg_sign' if opts[:gpg_sign] && opts[:no_gpg_sign]
|
|
1199
|
-
|
|
1200
|
-
ArgsBuilder.validate!(opts, COMMIT_OPTION_MAP)
|
|
1201
|
-
|
|
1202
|
-
args = build_args(opts, COMMIT_OPTION_MAP)
|
|
1203
|
-
command('commit', *args)
|
|
1222
|
+
opts = opts.merge(message: message) if message
|
|
1223
|
+
deprecate_commit_add_all_option!(opts)
|
|
1224
|
+
Git::Commands::Commit.new(self).call(no_edit: true, **opts).stdout
|
|
1204
1225
|
end
|
|
1205
|
-
RESET_OPTION_MAP = [
|
|
1206
|
-
{ keys: [:hard], flag: '--hard', type: :boolean }
|
|
1207
|
-
].freeze
|
|
1208
1226
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1227
|
+
# @return [String] the command output
|
|
1228
|
+
#
|
|
1229
|
+
def reset(commit = nil, opts = {})
|
|
1230
|
+
Git::Commands::Reset.new(self).call(commit, **opts).stdout
|
|
1213
1231
|
end
|
|
1214
1232
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
{ keys: [:ff], flag: '-ff', type: :boolean },
|
|
1218
|
-
{ keys: [:d], flag: '-d', type: :boolean },
|
|
1219
|
-
{ keys: [:x], flag: '-x', type: :boolean }
|
|
1220
|
-
].freeze
|
|
1221
|
-
|
|
1233
|
+
# @return [String] the command output
|
|
1234
|
+
#
|
|
1222
1235
|
def clean(opts = {})
|
|
1223
|
-
|
|
1224
|
-
|
|
1236
|
+
opts = migrate_clean_legacy_options(opts)
|
|
1237
|
+
Git::Commands::Clean.new(self).call(**opts).stdout
|
|
1225
1238
|
end
|
|
1226
1239
|
|
|
1227
|
-
|
|
1228
|
-
{ keys: [:no_edit], flag: '--no-edit', type: :boolean }
|
|
1229
|
-
].freeze
|
|
1240
|
+
REVERT_ALLOWED_OPTS = %i[no_edit].freeze
|
|
1230
1241
|
|
|
1231
1242
|
def revert(commitish, opts = {})
|
|
1232
|
-
|
|
1243
|
+
assert_valid_opts(opts, REVERT_ALLOWED_OPTS)
|
|
1233
1244
|
opts = { no_edit: true }.merge(opts)
|
|
1234
|
-
|
|
1235
|
-
args = build_args(opts, REVERT_OPTION_MAP)
|
|
1236
|
-
args << commitish
|
|
1237
|
-
|
|
1238
|
-
command('revert', *args)
|
|
1245
|
+
Git::Commands::Revert::Start.new(self).call(commitish, **opts).stdout
|
|
1239
1246
|
end
|
|
1240
1247
|
|
|
1241
1248
|
def apply(patch_file)
|
|
1242
|
-
|
|
1243
|
-
arr_opts << '--' << patch_file if patch_file
|
|
1244
|
-
command('apply', *arr_opts)
|
|
1249
|
+
Git::Commands::Apply.new(self).call(*[patch_file].compact, chdir: @git_work_dir).stdout
|
|
1245
1250
|
end
|
|
1246
1251
|
|
|
1247
1252
|
def apply_mail(patch_file)
|
|
1248
|
-
|
|
1249
|
-
arr_opts << '--' << patch_file if patch_file
|
|
1250
|
-
command('am', *arr_opts)
|
|
1253
|
+
Git::Commands::Am::Apply.new(self).call(*[patch_file].compact, chdir: @git_work_dir).stdout
|
|
1251
1254
|
end
|
|
1252
1255
|
|
|
1256
|
+
# Returns all stash entries as an array of index and message pairs
|
|
1257
|
+
#
|
|
1258
|
+
# List all stash entries in the repository ordered from oldest to newest
|
|
1259
|
+
#
|
|
1260
|
+
# The index is a sequential number starting from 0 for the oldest stash, and the
|
|
1261
|
+
# message is the description of the stash entry.
|
|
1262
|
+
#
|
|
1263
|
+
# @example List all stashes (oldest first)
|
|
1264
|
+
# lib.stashes_all # => [[0, "Fix bug"], [1, "Add feature"]]
|
|
1265
|
+
#
|
|
1266
|
+
# @return [Array<Array(Integer, String)>] array of [index, message] pairs where
|
|
1267
|
+
# index is the sequential position (0 is oldest) and message is the stash description
|
|
1268
|
+
#
|
|
1269
|
+
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1270
|
+
#
|
|
1253
1271
|
def stashes_all
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1272
|
+
result = Git::Commands::Stash::List.new(self).call
|
|
1273
|
+
stashes = Git::Parsers::Stash.parse_list(result.stdout)
|
|
1274
|
+
stashes.reverse.each_with_index.map { |info, i| stash_info_to_legacy(info, i) }
|
|
1257
1275
|
end
|
|
1258
1276
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1277
|
+
# Save the current working directory and index state to a new stash
|
|
1278
|
+
#
|
|
1279
|
+
# This method preserves v4.0.0 backward compatibility by returning a truthy/falsy
|
|
1280
|
+
# value indicating whether a stash was created.
|
|
1281
|
+
#
|
|
1282
|
+
# @param message [String] the stash message
|
|
1283
|
+
#
|
|
1284
|
+
# @return [Boolean] true if changes were stashed, false if there were no local changes to save
|
|
1285
|
+
#
|
|
1286
|
+
# @example Save current changes
|
|
1287
|
+
# lib.stash_save('WIP: feature work')
|
|
1288
|
+
#
|
|
1289
|
+
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1290
|
+
#
|
|
1291
|
+
def stash_save(message) # rubocop:disable Naming/PredicateMethod
|
|
1292
|
+
result = Git::Commands::Stash::Push.new(self).call(message: message)
|
|
1293
|
+
!result.stdout.include?('No local changes to save')
|
|
1262
1294
|
end
|
|
1263
1295
|
|
|
1296
|
+
# Apply a stash to the working directory
|
|
1297
|
+
#
|
|
1298
|
+
# This method preserves v4.0.0 backward compatibility by returning the command output.
|
|
1299
|
+
#
|
|
1300
|
+
# @param id [String, Integer, nil] the stash identifier (e.g., 'stash@\\{0}', 0) or nil for latest
|
|
1301
|
+
#
|
|
1302
|
+
# @return [String] the output from the git stash apply command
|
|
1303
|
+
#
|
|
1304
|
+
# @example Apply the latest stash
|
|
1305
|
+
# lib.stash_apply
|
|
1306
|
+
#
|
|
1307
|
+
# @example Apply a specific stash
|
|
1308
|
+
# lib.stash_apply('stash@{1}')
|
|
1309
|
+
#
|
|
1310
|
+
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1311
|
+
#
|
|
1264
1312
|
def stash_apply(id = nil)
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
else
|
|
1268
|
-
command('stash', 'apply')
|
|
1269
|
-
end
|
|
1313
|
+
result = Git::Commands::Stash::Apply.new(self).call(id)
|
|
1314
|
+
result.stdout
|
|
1270
1315
|
end
|
|
1271
1316
|
|
|
1317
|
+
# Remove all stash entries
|
|
1318
|
+
#
|
|
1319
|
+
# This method preserves v4.0.0 backward compatibility by returning the command output.
|
|
1320
|
+
#
|
|
1321
|
+
# @return [String] the output from the git stash clear command
|
|
1322
|
+
#
|
|
1323
|
+
# @example Clear all stashes
|
|
1324
|
+
# lib.stash_clear
|
|
1325
|
+
#
|
|
1326
|
+
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1327
|
+
#
|
|
1272
1328
|
def stash_clear
|
|
1273
|
-
|
|
1329
|
+
result = Git::Commands::Stash::Clear.new(self).call
|
|
1330
|
+
result.stdout
|
|
1274
1331
|
end
|
|
1275
1332
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1333
|
+
# List all stash entries in standard git stash list format
|
|
1334
|
+
#
|
|
1335
|
+
# This method preserves v4.0.0 backward compatibility by returning a formatted
|
|
1336
|
+
# string matching the output of `git stash list`.
|
|
1337
|
+
#
|
|
1338
|
+
# @return [String] newline-separated list of stash entries in the format
|
|
1339
|
+
# "stash@\\{n}: <message>", or an empty string if no stashes exist
|
|
1340
|
+
#
|
|
1341
|
+
# @example List all stashes
|
|
1342
|
+
# lib.stash_list # => "stash@\\{0}: On main: WIP\nstash@\\{1}: On feature: test"
|
|
1343
|
+
#
|
|
1344
|
+
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1345
|
+
#
|
|
1346
|
+
def stash_list
|
|
1347
|
+
result = Git::Commands::Stash::List.new(self).call
|
|
1348
|
+
stashes = Git::Parsers::Stash.parse_list(result.stdout)
|
|
1349
|
+
stashes.map { |info| "#{info.name}: #{info.message}" }.join("\n")
|
|
1278
1350
|
end
|
|
1279
1351
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1352
|
+
# Create a new branch
|
|
1353
|
+
#
|
|
1354
|
+
# @param branch [String] the name of the branch to create
|
|
1355
|
+
# @param start_point [String, nil] the commit, branch, or tag to start the new branch from
|
|
1356
|
+
# @param options [Hash] command options (see {Git::Commands::Branch::Create#call})
|
|
1357
|
+
#
|
|
1358
|
+
# @return [nil]
|
|
1359
|
+
#
|
|
1360
|
+
def branch_new(branch, start_point = nil, options = {})
|
|
1361
|
+
Git::Commands::Branch::Create.new(self).call(branch, start_point, **options)
|
|
1362
|
+
nil
|
|
1282
1363
|
end
|
|
1283
1364
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1365
|
+
# Delete one or more branches
|
|
1366
|
+
#
|
|
1367
|
+
# @param branches [Array<String>] the name(s) of the branch(es) to delete
|
|
1368
|
+
# @param options [Hash] command options (see {Git::Commands::Branch::Delete#call})
|
|
1369
|
+
# @option options [Boolean, nil] :force (nil) allow deleting unmerged branches (defaults to `true` when not given)
|
|
1370
|
+
# @option options [Boolean, nil] :remotes (nil) delete remote-tracking branches
|
|
1371
|
+
#
|
|
1372
|
+
# @return [String] newline-separated list of "Deleted branch <name> (was <sha>)." messages
|
|
1373
|
+
#
|
|
1374
|
+
# @raise [Git::Error] if any branch fails to delete
|
|
1375
|
+
#
|
|
1376
|
+
def branch_delete(*branches, **options)
|
|
1377
|
+
options = { force: true }.merge(options)
|
|
1378
|
+
result = Git::Commands::Branch::Delete.new(self).call(*branches, **options)
|
|
1287
1379
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
].freeze
|
|
1380
|
+
raise Git::Error, result.stderr.strip unless result.status.success?
|
|
1381
|
+
|
|
1382
|
+
result.stdout.strip
|
|
1383
|
+
end
|
|
1293
1384
|
|
|
1294
1385
|
# Runs checkout command to checkout or create branch
|
|
1295
1386
|
#
|
|
1296
1387
|
# accepts options:
|
|
1297
|
-
# :new_branch
|
|
1298
|
-
# :force
|
|
1299
|
-
# :start_point
|
|
1388
|
+
# :new_branch / :b - create a new branch with the given name (true = legacy, string = new)
|
|
1389
|
+
# :force / :f - proceed even with uncommitted changes
|
|
1390
|
+
# :start_point - start the new branch at this commit (used with :new_branch in legacy mode)
|
|
1391
|
+
#
|
|
1392
|
+
# @param [String] branch the branch to checkout, or nil
|
|
1393
|
+
# @param [Hash] opts options for the checkout command
|
|
1394
|
+
# @return [String] the command output
|
|
1300
1395
|
#
|
|
1301
|
-
# @param [String] branch
|
|
1302
|
-
# @param [Hash] opts
|
|
1303
1396
|
def checkout(branch = nil, opts = {})
|
|
1304
1397
|
if branch.is_a?(Hash) && opts.empty?
|
|
1305
1398
|
opts = branch
|
|
1306
1399
|
branch = nil
|
|
1307
1400
|
end
|
|
1308
|
-
ArgsBuilder.validate!(opts, CHECKOUT_OPTION_MAP)
|
|
1309
1401
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1402
|
+
target, translated_opts = translate_checkout_opts(branch, opts)
|
|
1403
|
+
Git::Commands::Checkout::Branch.new(self).call(target, **translated_opts).stdout
|
|
1404
|
+
end
|
|
1312
1405
|
|
|
1313
|
-
|
|
1406
|
+
# Translates legacy checkout options to the new command interface.
|
|
1407
|
+
# Legacy: checkout('branch', new_branch: true, start_point: 'main')
|
|
1408
|
+
# New: checkout('main', b: 'branch')
|
|
1409
|
+
def translate_checkout_opts(branch, opts)
|
|
1410
|
+
if opts[:new_branch] == true || opts[:b] == true
|
|
1411
|
+
[opts[:start_point], opts.except(:new_branch, :b, :start_point).merge(b: branch)]
|
|
1412
|
+
elsif opts[:new_branch].is_a?(String)
|
|
1413
|
+
[branch, opts.except(:new_branch).merge(b: opts[:new_branch])]
|
|
1414
|
+
else
|
|
1415
|
+
[branch, opts]
|
|
1416
|
+
end
|
|
1314
1417
|
end
|
|
1418
|
+
private :translate_checkout_opts
|
|
1315
1419
|
|
|
1420
|
+
# Checkout a specific version of a file
|
|
1421
|
+
#
|
|
1422
|
+
# @param version [String] the tree-ish (commit, branch, tag) to restore from
|
|
1423
|
+
# @param file [String] the file path to restore
|
|
1424
|
+
# @return [String] the command output
|
|
1425
|
+
#
|
|
1316
1426
|
def checkout_file(version, file)
|
|
1317
|
-
|
|
1318
|
-
arr_opts << version
|
|
1319
|
-
arr_opts << file
|
|
1320
|
-
command('checkout', *arr_opts)
|
|
1427
|
+
Git::Commands::Checkout::Files.new(self).call(version, pathspec: [file]).stdout
|
|
1321
1428
|
end
|
|
1322
1429
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
]
|
|
1328
|
-
|
|
1430
|
+
# Merge one or more branches into the current branch
|
|
1431
|
+
#
|
|
1432
|
+
# @param branch [String, Array<String>] branch name(s) to merge
|
|
1433
|
+
# @param message [String, nil] commit message for merge commit
|
|
1434
|
+
# @param opts [Hash] merge options
|
|
1435
|
+
#
|
|
1436
|
+
# @option opts [Boolean, nil] :no_commit (nil) stop before creating merge commit
|
|
1437
|
+
# (deprecated: use no_commit: true instead)
|
|
1438
|
+
# @option opts [Boolean, nil] :no_ff (nil) create merge commit even for fast-forward
|
|
1439
|
+
# (deprecated: use no_ff: true instead)
|
|
1440
|
+
# @option opts [String] :m (nil) commit message (deprecated: use message: option)
|
|
1441
|
+
# @option opts [Boolean, nil] :commit (nil) true for --commit (`--commit`)
|
|
1442
|
+
# @option opts [Boolean, nil] :ff (nil) true for --ff (`--ff`)
|
|
1443
|
+
# @option opts [Boolean, nil] :ff_only (nil) only merge if fast-forward possible
|
|
1444
|
+
# @option opts [Boolean, nil] :squash (nil) squash commits into single commit
|
|
1445
|
+
# @option opts [String] :message (nil) commit message
|
|
1446
|
+
# @option opts [String] :strategy (nil) merge strategy (e.g., 'ort', 'ours')
|
|
1447
|
+
# @option opts [String, Array<String>] :strategy_option (nil) strategy-specific options
|
|
1448
|
+
# @option opts [Boolean, nil] :allow_unrelated_histories (nil) allow merging unrelated histories
|
|
1449
|
+
#
|
|
1450
|
+
# @return [String] the command output
|
|
1451
|
+
#
|
|
1329
1452
|
def merge(branch, message = nil, opts = {})
|
|
1330
|
-
#
|
|
1331
|
-
opts
|
|
1332
|
-
ArgsBuilder.validate!(opts, MERGE_OPTION_MAP)
|
|
1453
|
+
# Handle legacy positional message argument
|
|
1454
|
+
opts = opts.merge(message: message) if message
|
|
1333
1455
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1456
|
+
# Map legacy option names to new interface
|
|
1457
|
+
opts = translate_merge_options(opts)
|
|
1336
1458
|
|
|
1337
|
-
|
|
1459
|
+
Git::Commands::Merge::Start.new(self).call(*Array(branch), no_edit: true, **opts).stdout
|
|
1338
1460
|
end
|
|
1339
1461
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1462
|
+
# Find common ancestor commit(s) for merge
|
|
1463
|
+
#
|
|
1464
|
+
# @overload merge_base(*commits, options = {})
|
|
1465
|
+
#
|
|
1466
|
+
# @param commits [Array<String>] commits to find common ancestor(s) of
|
|
1467
|
+
#
|
|
1468
|
+
# @param options [Hash] merge-base options
|
|
1469
|
+
#
|
|
1470
|
+
# @option options [Boolean, nil] :octopus (nil) compute best ancestor for n-way merge
|
|
1471
|
+
# @option options [Boolean, nil] :independent (nil) list commits not reachable from others
|
|
1472
|
+
# @option options [Boolean, nil] :fork_point (nil) find fork point
|
|
1473
|
+
# @option options [Boolean, nil] :all (nil) output all merge bases
|
|
1474
|
+
#
|
|
1475
|
+
# @return [Array<String>] array of commit SHAs
|
|
1476
|
+
#
|
|
1347
1477
|
def merge_base(*args)
|
|
1348
1478
|
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
flags = build_args(opts, MERGE_BASE_OPTION_MAP)
|
|
1352
|
-
command_args = flags + args
|
|
1353
|
-
|
|
1354
|
-
command('merge-base', *command_args).lines.map(&:strip)
|
|
1479
|
+
result = Git::Commands::MergeBase.new(self).call(*args, **opts)
|
|
1480
|
+
result.stdout.lines.map(&:strip).reject(&:empty?)
|
|
1355
1481
|
end
|
|
1356
1482
|
|
|
1483
|
+
# List paths that remain unmerged after a failed or partial merge
|
|
1484
|
+
#
|
|
1485
|
+
# Delegates to {Git::Commands::Diff}.
|
|
1486
|
+
#
|
|
1487
|
+
# @return [Array<String>] paths of files with unresolved merge conflicts
|
|
1488
|
+
#
|
|
1489
|
+
# @raise [Git::FailedError] if git returns exit code > 2
|
|
1490
|
+
#
|
|
1491
|
+
# @see Git::Commands::Diff
|
|
1492
|
+
#
|
|
1357
1493
|
def unmerged
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1494
|
+
result = Git::Commands::Diff.new(self).call(cached: true)
|
|
1495
|
+
result.stdout.split("\n").filter_map do |line|
|
|
1496
|
+
::Regexp.last_match(1) if line =~ /^\* Unmerged path (.*)/
|
|
1361
1497
|
end
|
|
1362
|
-
unmerged
|
|
1363
1498
|
end
|
|
1364
1499
|
|
|
1365
1500
|
def conflicts # :yields: file, your, their
|
|
@@ -1375,416 +1510,961 @@ module Git
|
|
|
1375
1510
|
end
|
|
1376
1511
|
end
|
|
1377
1512
|
|
|
1378
|
-
REMOTE_ADD_OPTION_MAP = [
|
|
1379
|
-
{ keys: %i[with_fetch fetch], flag: '-f', type: :boolean },
|
|
1380
|
-
{ keys: [:track], flag: '-t', type: :valued_space }
|
|
1381
|
-
].freeze
|
|
1382
|
-
|
|
1383
1513
|
def remote_add(name, url, opts = {})
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
flags = build_args(opts, REMOTE_ADD_OPTION_MAP)
|
|
1387
|
-
positional_args = ['--', name, url]
|
|
1388
|
-
command_args = ['add'] + flags + positional_args
|
|
1514
|
+
translated_opts = opts.dup
|
|
1515
|
+
translated_opts[:fetch] = translated_opts.delete(:with_fetch) if translated_opts.key?(:with_fetch)
|
|
1389
1516
|
|
|
1390
|
-
|
|
1517
|
+
Git::Commands::Remote::Add.new(self).call(name, url, **translated_opts)
|
|
1391
1518
|
end
|
|
1392
1519
|
|
|
1393
|
-
REMOTE_SET_BRANCHES_OPTION_MAP = [
|
|
1394
|
-
{ keys: [:add], flag: '--add', type: :boolean }
|
|
1395
|
-
].freeze
|
|
1396
|
-
|
|
1397
1520
|
def remote_set_branches(name, branches, opts = {})
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
flags = build_args(opts, REMOTE_SET_BRANCHES_OPTION_MAP)
|
|
1401
|
-
branch_args = Array(branches).flatten
|
|
1402
|
-
command_args = ['set-branches'] + flags + [name] + branch_args
|
|
1403
|
-
|
|
1404
|
-
command('remote', *command_args)
|
|
1521
|
+
Git::Commands::Remote::SetBranches.new(self).call(name, *Array(branches).flatten, **opts)
|
|
1405
1522
|
end
|
|
1406
1523
|
|
|
1407
|
-
def remote_set_url(name, url)
|
|
1408
|
-
|
|
1409
|
-
arr_opts << name
|
|
1410
|
-
arr_opts << url
|
|
1411
|
-
|
|
1412
|
-
command('remote', *arr_opts)
|
|
1524
|
+
def remote_set_url(name, url, opts = {})
|
|
1525
|
+
Git::Commands::Remote::SetUrl.new(self).call(name, url, **opts)
|
|
1413
1526
|
end
|
|
1414
1527
|
|
|
1415
1528
|
def remote_remove(name)
|
|
1416
|
-
|
|
1529
|
+
Git::Commands::Remote::Remove.new(self).call(name)
|
|
1417
1530
|
end
|
|
1418
1531
|
|
|
1419
1532
|
def remotes
|
|
1420
|
-
|
|
1533
|
+
Git::Commands::Remote::List.new(self).call.stdout.split("\n")
|
|
1421
1534
|
end
|
|
1422
1535
|
|
|
1536
|
+
# List all tags in the repository
|
|
1537
|
+
#
|
|
1538
|
+
# @see https://git-scm.com/docs/git-tag git-tag
|
|
1539
|
+
#
|
|
1540
|
+
# @return [Array<String>] tag names
|
|
1541
|
+
#
|
|
1423
1542
|
def tags
|
|
1424
|
-
|
|
1543
|
+
result = Git::Commands::Tag::List.new(self).call(format: Git::Parsers::Tag::FORMAT_STRING)
|
|
1544
|
+
Git::Parsers::Tag.parse_list(result.stdout).map(&:name)
|
|
1425
1545
|
end
|
|
1426
1546
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1547
|
+
# Create or delete a tag
|
|
1548
|
+
#
|
|
1549
|
+
# When the `:d` or `:delete` option is set, deletes the named tag.
|
|
1550
|
+
# Otherwise, creates a new tag pointing at HEAD or the specified target.
|
|
1551
|
+
#
|
|
1552
|
+
# @see https://git-scm.com/docs/git-tag git-tag
|
|
1553
|
+
#
|
|
1554
|
+
# @overload tag(name, target, opts = {})
|
|
1555
|
+
#
|
|
1556
|
+
# Create a tag on the specified target
|
|
1557
|
+
#
|
|
1558
|
+
# @param name [String] the tag name to create
|
|
1559
|
+
#
|
|
1560
|
+
# @param target [String] the commit or object to tag
|
|
1561
|
+
#
|
|
1562
|
+
# @param opts [Hash] options for creating the tag
|
|
1563
|
+
#
|
|
1564
|
+
# @option opts [Boolean, nil] :annotate (nil) create an unsigned, annotated tag object.
|
|
1565
|
+
# Requires `:message` or `:file`.
|
|
1566
|
+
#
|
|
1567
|
+
# Alias: `:a`
|
|
1568
|
+
#
|
|
1569
|
+
# @option opts [Boolean, nil] :sign (nil) create a GPG-signed tag. Requires `:message` or `:file`.
|
|
1570
|
+
#
|
|
1571
|
+
# Alias: `:s`
|
|
1572
|
+
#
|
|
1573
|
+
# @option opts [Boolean, nil] :force (nil) replace an existing tag with the given name.
|
|
1574
|
+
#
|
|
1575
|
+
# Alias: `:f`
|
|
1576
|
+
#
|
|
1577
|
+
# @option opts [String] :message (nil) use the given string as the tag message.
|
|
1578
|
+
# Implies annotated tag if none of `:annotate`, `:sign`, or `:local_user` is given.
|
|
1579
|
+
#
|
|
1580
|
+
# Alias: `:m`
|
|
1581
|
+
#
|
|
1582
|
+
# @overload tag(name, opts = {})
|
|
1583
|
+
#
|
|
1584
|
+
# Create a lightweight tag on HEAD
|
|
1585
|
+
#
|
|
1586
|
+
# @param name [String] the tag name to create
|
|
1587
|
+
#
|
|
1588
|
+
# @param opts [Hash] options for creating the tag
|
|
1589
|
+
#
|
|
1590
|
+
# @option opts [Boolean, nil] :annotate (nil) create an unsigned, annotated tag object.
|
|
1591
|
+
# Requires `:message` or `:file`.
|
|
1592
|
+
#
|
|
1593
|
+
# Alias: `:a`
|
|
1594
|
+
#
|
|
1595
|
+
# @option opts [Boolean, nil] :sign (nil) create a GPG-signed tag. Requires `:message` or `:file`.
|
|
1596
|
+
#
|
|
1597
|
+
# Alias: `:s`
|
|
1598
|
+
#
|
|
1599
|
+
# @option opts [Boolean, nil] :force (nil) replace an existing tag with the given name.
|
|
1600
|
+
#
|
|
1601
|
+
# Alias: `:f`
|
|
1602
|
+
#
|
|
1603
|
+
# @option opts [String] :message (nil) use the given string as the tag message.
|
|
1604
|
+
# Implies annotated tag if none of `:annotate`, `:sign`, or `:local_user` is given.
|
|
1605
|
+
#
|
|
1606
|
+
# Alias: `:m`
|
|
1607
|
+
#
|
|
1608
|
+
# @overload tag(name, opts = {})
|
|
1609
|
+
#
|
|
1610
|
+
# Delete the named tag
|
|
1611
|
+
#
|
|
1612
|
+
# @param name [String] the tag name to delete
|
|
1613
|
+
#
|
|
1614
|
+
# @param opts [Hash] options
|
|
1615
|
+
#
|
|
1616
|
+
# @option opts [Boolean, nil] :delete (nil) delete the named tag.
|
|
1617
|
+
#
|
|
1618
|
+
# Alias: `:d`
|
|
1619
|
+
#
|
|
1620
|
+
# @return [String] command output
|
|
1621
|
+
#
|
|
1622
|
+
# @raise [ArgumentError] if creating an annotated or signed tag without a message
|
|
1623
|
+
#
|
|
1624
|
+
# @raise [Git::FailedError] if the tag already exists (without `:force`) or if
|
|
1625
|
+
# the tag to delete does not exist
|
|
1626
|
+
#
|
|
1435
1627
|
def tag(name, *args)
|
|
1436
1628
|
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
1437
1629
|
target = args.first
|
|
1438
1630
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1631
|
+
if opts[:d] || opts[:delete]
|
|
1632
|
+
delete_tag(name)
|
|
1633
|
+
else
|
|
1634
|
+
validate_tag_options!(opts)
|
|
1635
|
+
create_tag(name, target, opts)
|
|
1636
|
+
end
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
def fetch(remote, opts)
|
|
1640
|
+
opts = opts.dup
|
|
1641
|
+
refspecs = Array(opts.delete(:ref)).compact
|
|
1642
|
+
positionals = [*([remote] if remote), *refspecs]
|
|
1643
|
+
Git::Commands::Fetch.new(self).call(*positionals, **opts, merge: true).stdout
|
|
1644
|
+
end
|
|
1645
|
+
|
|
1646
|
+
PUSH_ALLOWED_OPTS = %i[mirror delete force f push_option all tags].freeze
|
|
1647
|
+
|
|
1648
|
+
# Push refs to a remote repository
|
|
1649
|
+
#
|
|
1650
|
+
# @overload push(options = {})
|
|
1651
|
+
# Push using the current branch's default remote and push configuration
|
|
1652
|
+
#
|
|
1653
|
+
# @param options [Hash] push options
|
|
1654
|
+
#
|
|
1655
|
+
# @option options [Boolean, nil] :all (nil) push all branches
|
|
1656
|
+
#
|
|
1657
|
+
# @option options [Boolean, nil] :mirror (nil) push all refs
|
|
1658
|
+
#
|
|
1659
|
+
# @option options [Boolean, nil] :tags (nil) push all tags
|
|
1660
|
+
#
|
|
1661
|
+
# @option options [Boolean, nil] :force (nil) force updates
|
|
1662
|
+
#
|
|
1663
|
+
# @option options [Boolean, nil] :delete (nil) delete the named remote ref
|
|
1664
|
+
#
|
|
1665
|
+
# @option options [String, Array<String>] :push_option (nil) server-side push option values
|
|
1666
|
+
#
|
|
1667
|
+
# @return [String] the stdout from the final `git push` invocation
|
|
1668
|
+
#
|
|
1669
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
1670
|
+
#
|
|
1671
|
+
# @overload push(remote, options = {})
|
|
1672
|
+
# Push to the given remote using the current branch's default push configuration
|
|
1673
|
+
#
|
|
1674
|
+
# @param remote [String] the remote name or URL to push to
|
|
1675
|
+
#
|
|
1676
|
+
# @param options [Hash] push options
|
|
1677
|
+
#
|
|
1678
|
+
# @option options [Boolean, nil] :all (nil) push all branches
|
|
1679
|
+
#
|
|
1680
|
+
# @option options [Boolean, nil] :mirror (nil) push all refs
|
|
1681
|
+
#
|
|
1682
|
+
# @option options [Boolean, nil] :tags (nil) push all tags
|
|
1683
|
+
#
|
|
1684
|
+
# @option options [Boolean, nil] :force (nil) force updates
|
|
1685
|
+
#
|
|
1686
|
+
# @option options [Boolean, nil] :delete (nil) delete the named remote ref
|
|
1687
|
+
#
|
|
1688
|
+
# @option options [String, Array<String>] :push_option (nil) server-side push option values
|
|
1689
|
+
#
|
|
1690
|
+
# @return [String] the stdout from the final `git push` invocation
|
|
1691
|
+
#
|
|
1692
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
1693
|
+
#
|
|
1694
|
+
# @overload push(remote, branch, options = {})
|
|
1695
|
+
# Push a branch or refspec to the given remote
|
|
1696
|
+
#
|
|
1697
|
+
# @param remote [String] the remote name or URL to push to
|
|
1698
|
+
#
|
|
1699
|
+
# @param branch [String] the branch name or refspec to push
|
|
1700
|
+
#
|
|
1701
|
+
# @param options [Hash] push options
|
|
1702
|
+
#
|
|
1703
|
+
# @option options [Boolean, nil] :all (nil) push all branches
|
|
1704
|
+
#
|
|
1705
|
+
# @option options [Boolean, nil] :mirror (nil) push all refs
|
|
1706
|
+
#
|
|
1707
|
+
# @option options [Boolean, nil] :tags (nil) push all tags
|
|
1708
|
+
#
|
|
1709
|
+
# @option options [Boolean, nil] :force (nil) force updates
|
|
1710
|
+
#
|
|
1711
|
+
# @option options [Boolean, nil] :delete (nil) delete the named remote ref
|
|
1712
|
+
#
|
|
1713
|
+
# @option options [String, Array<String>] :push_option (nil) server-side push option values
|
|
1714
|
+
#
|
|
1715
|
+
# @return [String] the stdout from the final `git push` invocation
|
|
1716
|
+
#
|
|
1717
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
1718
|
+
#
|
|
1719
|
+
# @raise [ArgumentError] if `remote` is nil
|
|
1720
|
+
#
|
|
1721
|
+
# @overload push(remote, branch, tags)
|
|
1722
|
+
# Backward-compatible shorthand for `push(remote, branch, tags: tags)`
|
|
1723
|
+
#
|
|
1724
|
+
# @param remote [String] the remote name or URL to push to
|
|
1725
|
+
#
|
|
1726
|
+
# @param branch [String] the branch name or refspec to push
|
|
1727
|
+
#
|
|
1728
|
+
# @param tags [Boolean] whether to push all tags
|
|
1729
|
+
#
|
|
1730
|
+
# @return [String] the stdout from the final `git push` invocation
|
|
1731
|
+
#
|
|
1732
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
1733
|
+
#
|
|
1734
|
+
# @raise [ArgumentError] if `remote` is nil
|
|
1735
|
+
#
|
|
1736
|
+
def push(remote = nil, branch = nil, opts = nil)
|
|
1737
|
+
remote, branch, opts = normalize_push_args(remote, branch, opts)
|
|
1738
|
+
validate_push_args!(remote, branch, opts)
|
|
1441
1739
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1740
|
+
first_result = push_refs(remote, branch, opts)
|
|
1741
|
+
return first_result.stdout unless push_tags_separately?(opts)
|
|
1444
1742
|
|
|
1445
|
-
|
|
1743
|
+
push_tags(remote, opts).stdout
|
|
1446
1744
|
end
|
|
1447
1745
|
|
|
1448
|
-
|
|
1449
|
-
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1450
|
-
{ keys: %i[tags t], flag: '--tags', type: :boolean },
|
|
1451
|
-
{ keys: %i[prune p], flag: '--prune', type: :boolean },
|
|
1452
|
-
{ keys: %i[prune-tags P], flag: '--prune-tags', type: :boolean },
|
|
1453
|
-
{ keys: %i[force f], flag: '--force', type: :boolean },
|
|
1454
|
-
{ keys: %i[update-head-ok u], flag: '--update-head-ok', type: :boolean },
|
|
1455
|
-
{ keys: [:unshallow], flag: '--unshallow', type: :boolean },
|
|
1456
|
-
{ keys: [:depth], flag: '--depth', type: :valued_space },
|
|
1457
|
-
{ keys: [:ref], type: :validate_only }
|
|
1458
|
-
].freeze
|
|
1746
|
+
PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
|
|
1459
1747
|
|
|
1460
|
-
def
|
|
1461
|
-
|
|
1462
|
-
|
|
1748
|
+
def pull(remote = nil, branch = nil, opts = {})
|
|
1749
|
+
raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?
|
|
1750
|
+
|
|
1751
|
+
assert_valid_opts(opts, PULL_ALLOWED_OPTS)
|
|
1752
|
+
allowed_opts = opts.slice(*PULL_ALLOWED_OPTS)
|
|
1753
|
+
positional_args = [remote, branch].compact
|
|
1754
|
+
Git::Commands::Pull.new(self).call(*positional_args, no_edit: true, no_progress: true, **allowed_opts).stdout
|
|
1755
|
+
end
|
|
1463
1756
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1757
|
+
# Return the SHA of a tag reference
|
|
1758
|
+
#
|
|
1759
|
+
# Looks up the tag first in the local refs directory, then falls back to
|
|
1760
|
+
# `git show-ref`. Returns an empty string if the tag does not exist.
|
|
1761
|
+
#
|
|
1762
|
+
# @param tag_name [String] the tag name to look up
|
|
1763
|
+
#
|
|
1764
|
+
# @return [String] the SHA of the tag, or an empty string if not found
|
|
1765
|
+
#
|
|
1766
|
+
def tag_sha(tag_name)
|
|
1767
|
+
head = File.join(@git_dir, 'refs', 'tags', tag_name)
|
|
1768
|
+
return File.read(head).chomp if File.exist?(head)
|
|
1769
|
+
|
|
1770
|
+
result = Git::Commands::ShowRef::List.new(self).call(tag_name, tags: true, hash: true)
|
|
1771
|
+
result.stdout
|
|
1772
|
+
end
|
|
1773
|
+
|
|
1774
|
+
def repack
|
|
1775
|
+
Git::Commands::Repack.new(self).call(a: true, d: true)
|
|
1776
|
+
end
|
|
1777
|
+
|
|
1778
|
+
def gc
|
|
1779
|
+
Git::Commands::Gc.new(self).call(prune: true, aggressive: true, auto: true)
|
|
1780
|
+
end
|
|
1781
|
+
|
|
1782
|
+
# Execute git fsck to verify repository integrity
|
|
1783
|
+
#
|
|
1784
|
+
# @param objects [Array<String>] optional object identifiers to check
|
|
1785
|
+
# @param opts [Hash] command options (see {Git::Commands::Fsck#call})
|
|
1786
|
+
#
|
|
1787
|
+
# @return [Git::FsckResult] the structured result
|
|
1788
|
+
#
|
|
1789
|
+
# rubocop:disable Style/ArgumentsForwarding
|
|
1790
|
+
def fsck(*objects, **opts)
|
|
1791
|
+
result = Git::Commands::Fsck.new(self).call(*objects, no_progress: true, **opts)
|
|
1792
|
+
Git::Parsers::Fsck.parse(result.stdout)
|
|
1793
|
+
end
|
|
1794
|
+
# rubocop:enable Style/ArgumentsForwarding
|
|
1795
|
+
|
|
1796
|
+
READ_TREE_ALLOWED_OPTS = %i[prefix].freeze
|
|
1797
|
+
|
|
1798
|
+
def read_tree(treeish, opts = {})
|
|
1799
|
+
assert_valid_opts(opts, READ_TREE_ALLOWED_OPTS)
|
|
1800
|
+
allowed_opts = opts.slice(*READ_TREE_ALLOWED_OPTS)
|
|
1801
|
+
Git::Commands::ReadTree.new(self).call(treeish, **allowed_opts)
|
|
1802
|
+
end
|
|
1803
|
+
|
|
1804
|
+
def write_tree
|
|
1805
|
+
Git::Commands::WriteTree.new(self).call.stdout
|
|
1806
|
+
end
|
|
1807
|
+
|
|
1808
|
+
COMMIT_TREE_ALLOWED_OPTS = %i[p parent parents m message].freeze
|
|
1809
|
+
|
|
1810
|
+
def commit_tree(tree, opts = {})
|
|
1811
|
+
assert_valid_opts(opts, COMMIT_TREE_ALLOWED_OPTS)
|
|
1812
|
+
actual_opts = normalize_commit_tree_opts(opts, tree)
|
|
1813
|
+
Git::Commands::CommitTree.new(self).call(tree, **actual_opts).stdout
|
|
1814
|
+
end
|
|
1815
|
+
|
|
1816
|
+
def update_ref(ref, commit)
|
|
1817
|
+
Git::Commands::UpdateRef::Update.new(self).call(ref, commit)
|
|
1818
|
+
end
|
|
1819
|
+
|
|
1820
|
+
def checkout_index(opts = {})
|
|
1821
|
+
paths = normalize_pathspecs(opts[:path_limiter], 'path_limiter')
|
|
1822
|
+
keyword_opts = opts.except(:path_limiter)
|
|
1823
|
+
Git::Commands::CheckoutIndex.new(self).call(*paths.to_a, **keyword_opts)
|
|
1824
|
+
end
|
|
1825
|
+
|
|
1826
|
+
ARCHIVE_ALLOWED_OPTS = %i[prefix remote path format add_gzip].freeze
|
|
1827
|
+
|
|
1828
|
+
# Creates an archive of the given tree-ish and writes it to a file
|
|
1829
|
+
#
|
|
1830
|
+
# Delegates to {Git::Commands::Archive} for CLI execution. Format coercion
|
|
1831
|
+
# (`tgz` → `tar` + gzip), temp file management, and gzip post-processing
|
|
1832
|
+
# remain in this adapter.
|
|
1833
|
+
#
|
|
1834
|
+
# @see https://git-scm.com/docs/git-archive git-archive
|
|
1835
|
+
#
|
|
1836
|
+
# @param sha [String] tree-ish to archive (commit, tag, branch, or tree SHA)
|
|
1837
|
+
#
|
|
1838
|
+
# @param file [String, nil] destination file path; a unique temp file is
|
|
1839
|
+
# created and returned if `nil`
|
|
1840
|
+
#
|
|
1841
|
+
# @param opts [Hash] archive options
|
|
1842
|
+
#
|
|
1843
|
+
# @option opts [String] :prefix prefix to prepend to each filename in the archive
|
|
1844
|
+
#
|
|
1845
|
+
# @option opts [String] :remote URL of a remote repository to archive from
|
|
1846
|
+
#
|
|
1847
|
+
# @option opts [String] :path limit the archive to a path within the tree
|
|
1848
|
+
#
|
|
1849
|
+
# @option opts [String] :format archive format — `'tar'`, `'tgz'`, or `'zip'`
|
|
1850
|
+
# (default: `'zip'`)
|
|
1851
|
+
#
|
|
1852
|
+
# @option opts [Boolean, nil] :add_gzip (nil) wrap the archive in gzip compression
|
|
1853
|
+
#
|
|
1854
|
+
# @return [String] the path to the written archive file
|
|
1855
|
+
#
|
|
1856
|
+
# @raise [Git::FailedError] if `git archive` fails
|
|
1857
|
+
#
|
|
1858
|
+
def archive(sha, file = nil, opts = {})
|
|
1859
|
+
assert_valid_opts(opts, ARCHIVE_ALLOWED_OPTS)
|
|
1860
|
+
file ||= temp_file_name
|
|
1861
|
+
format, gzip = parse_archive_format_options(opts)
|
|
1862
|
+
|
|
1863
|
+
command_opts = opts.slice(:prefix, :remote).merge(format: format)
|
|
1864
|
+
path_args = opts[:path] ? [opts[:path]] : []
|
|
1865
|
+
|
|
1866
|
+
File.open(file, 'wb') do |f|
|
|
1867
|
+
Git::Commands::Archive.new(self).call(sha, *path_args, **command_opts, out: f)
|
|
1468
1868
|
end
|
|
1869
|
+
apply_gzip(file) if gzip
|
|
1469
1870
|
|
|
1470
|
-
|
|
1871
|
+
file
|
|
1471
1872
|
end
|
|
1472
1873
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1874
|
+
# Returns the git version as a Git::Version
|
|
1875
|
+
#
|
|
1876
|
+
# Parses the output of `git version`, strips platform suffixes (like
|
|
1877
|
+
# `.windows.1` or `.vfs.0`), and pads two-segment versions to three segments.
|
|
1878
|
+
#
|
|
1879
|
+
# Results are cached globally (keyed by binary path). It is assumed that the
|
|
1880
|
+
# git version doesn't change during runtime for a given binary.
|
|
1881
|
+
#
|
|
1882
|
+
# @return [Git::Version] the parsed git version
|
|
1883
|
+
#
|
|
1884
|
+
# @raise [Git::UnexpectedResultError] if the version string cannot be parsed
|
|
1885
|
+
#
|
|
1886
|
+
# @example
|
|
1887
|
+
# lib.git_version #=> Git::Version.new(2, 42, 1)
|
|
1888
|
+
#
|
|
1889
|
+
def git_version
|
|
1890
|
+
self.class.cached_git_version(Git::Base.config.binary_path) do
|
|
1891
|
+
output = Git::Commands::Version.new(self).call.stdout
|
|
1892
|
+
Git::Version.parse(output)
|
|
1893
|
+
end
|
|
1894
|
+
end
|
|
1481
1895
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1896
|
+
# Class-level cache for git versions, keyed by binary path
|
|
1897
|
+
#
|
|
1898
|
+
# Thread-safe for JRuby/TruffleRuby where true parallelism exists.
|
|
1899
|
+
#
|
|
1900
|
+
# @api private
|
|
1901
|
+
#
|
|
1902
|
+
def self.cached_git_version(binary_path, &block)
|
|
1903
|
+
@git_version_cache_mutex.synchronize do
|
|
1904
|
+
@git_version_cache[binary_path] ||= block.call
|
|
1905
|
+
end
|
|
1906
|
+
end
|
|
1485
1907
|
|
|
1486
|
-
|
|
1908
|
+
# Clear the git version cache (primarily for testing)
|
|
1909
|
+
#
|
|
1910
|
+
# @api private
|
|
1911
|
+
#
|
|
1912
|
+
def self.clear_git_version_cache
|
|
1913
|
+
@git_version_cache_mutex.synchronize do
|
|
1914
|
+
@git_version_cache.clear
|
|
1915
|
+
end
|
|
1916
|
+
end
|
|
1487
1917
|
|
|
1488
|
-
|
|
1918
|
+
# Returns the current version of git, as an Array<Integer>
|
|
1919
|
+
#
|
|
1920
|
+
# @deprecated Use {Git.git_version} instead, which returns a {Git::Version} (not an Array).
|
|
1921
|
+
# For the legacy array shape, call: `Git.git_version.to_a`
|
|
1922
|
+
#
|
|
1923
|
+
def current_command_version
|
|
1924
|
+
Git::Deprecation.warn(
|
|
1925
|
+
'Git::Lib#current_command_version is deprecated and will be removed in 6.0. ' \
|
|
1926
|
+
'Use Git.git_version instead, which returns a Git::Version (not an Array). ' \
|
|
1927
|
+
'For the legacy array shape, call: Git.git_version.to_a'
|
|
1928
|
+
)
|
|
1929
|
+
git_version.to_a
|
|
1930
|
+
end
|
|
1489
1931
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1932
|
+
# Returns current_command_version <=> other_version
|
|
1933
|
+
#
|
|
1934
|
+
# @example
|
|
1935
|
+
# lib.compare_version_to(2, 41, 0) #=> 1
|
|
1936
|
+
# lib.compare_version_to(2, 42, 0) #=> 0
|
|
1937
|
+
# lib.compare_version_to(2, 43, 0) #=> -1
|
|
1938
|
+
#
|
|
1939
|
+
# @param other_version [Array<Integer>] the other version to compare to
|
|
1940
|
+
# @return [Integer] -1 if this version is less than other_version, 0 if equal, or 1 if greater than
|
|
1941
|
+
#
|
|
1942
|
+
# @deprecated Use {Git.git_version} with {Git::Version} comparison operators instead,
|
|
1943
|
+
# e.g. `Git.git_version <=> Git::Version.new(2, 41, 0)`
|
|
1944
|
+
#
|
|
1945
|
+
def compare_version_to(*other_version)
|
|
1946
|
+
Git::Deprecation.warn(
|
|
1947
|
+
'Git::Lib#compare_version_to is deprecated and will be removed in 6.0. ' \
|
|
1948
|
+
'Use Git.git_version with Git::Version comparison operators instead, ' \
|
|
1949
|
+
'e.g. Git.git_version <=> Git::Version.new(2, 41, 0)'
|
|
1950
|
+
)
|
|
1951
|
+
git_version.to_a <=> other_version
|
|
1952
|
+
end
|
|
1953
|
+
|
|
1954
|
+
# @deprecated Use {Git::MINIMUM_GIT_VERSION} constant instead, which returns a {Git::Version}
|
|
1955
|
+
# (not an Array). For the legacy array shape, call: `Git::MINIMUM_GIT_VERSION.to_a.first(2)`
|
|
1956
|
+
#
|
|
1957
|
+
def required_command_version
|
|
1958
|
+
Git::Deprecation.warn(
|
|
1959
|
+
'Git::Lib#required_command_version is deprecated and will be removed in 6.0. ' \
|
|
1960
|
+
'Use the Git::MINIMUM_GIT_VERSION constant instead, which returns a Git::Version ' \
|
|
1961
|
+
'(not an Array). For the legacy array shape, call: Git::MINIMUM_GIT_VERSION.to_a.first(2)'
|
|
1962
|
+
)
|
|
1963
|
+
Git::MINIMUM_GIT_VERSION.to_a.first(2)
|
|
1964
|
+
end
|
|
1965
|
+
|
|
1966
|
+
# @deprecated For a boolean check, use `Git.git_version >= Git::MINIMUM_GIT_VERSION`.
|
|
1967
|
+
# For enforcement, no action is needed: {Git::Commands::Base#call} automatically
|
|
1968
|
+
# invokes `validate_version!`, which raises {Git::VersionError} on failure.
|
|
1969
|
+
#
|
|
1970
|
+
def meets_required_version?
|
|
1971
|
+
Git::Deprecation.warn(
|
|
1972
|
+
'Git::Lib#meets_required_version? is deprecated and will be removed in 6.0. ' \
|
|
1973
|
+
'For a boolean check, use: Git.git_version >= Git::MINIMUM_GIT_VERSION. ' \
|
|
1974
|
+
'For enforcement, no action is needed: Git::Commands::Base#call automatically ' \
|
|
1975
|
+
'invokes validate_version!, which raises Git::VersionError on failure.'
|
|
1976
|
+
)
|
|
1977
|
+
git_version >= Git::MINIMUM_GIT_VERSION
|
|
1978
|
+
end
|
|
1979
|
+
|
|
1980
|
+
# @deprecated Version validation is now handled automatically by
|
|
1981
|
+
# {Git::Commands::Base#validate_version!}, which raises {Git::VersionError} on failure.
|
|
1982
|
+
# Callers wanting the old warn-and-continue behavior must implement it themselves
|
|
1983
|
+
# using: `Git.git_version >= Git::MINIMUM_GIT_VERSION`.
|
|
1984
|
+
#
|
|
1985
|
+
def self.warn_if_old_command(_lib) # rubocop:disable Metrics/MethodLength, Naming/PredicateMethod
|
|
1986
|
+
Git::Deprecation.warn(
|
|
1987
|
+
'Git::Lib.warn_if_old_command is deprecated and will be removed in 6.0. ' \
|
|
1988
|
+
'Version validation is now handled automatically by Git::Commands::Base#validate_version!, ' \
|
|
1989
|
+
'which RAISES Git::VersionError on failure (the old method only printed a warning ' \
|
|
1990
|
+
'once per process and continued). Callers wanting the old warn-and-continue behavior ' \
|
|
1991
|
+
'must implement it themselves using: Git.git_version >= Git::MINIMUM_GIT_VERSION.'
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
return true if @version_checked
|
|
1995
|
+
|
|
1996
|
+
@version_checked = true
|
|
1997
|
+
git_version = Git.git_version
|
|
1998
|
+
unless git_version >= Git::MINIMUM_GIT_VERSION
|
|
1999
|
+
warn "The git gem requires git #{Git::MINIMUM_GIT_VERSION} or later, " \
|
|
2000
|
+
"but only found #{git_version}. You should probably upgrade."
|
|
1495
2001
|
end
|
|
2002
|
+
true
|
|
1496
2003
|
end
|
|
1497
2004
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
2005
|
+
COMMAND_CAPTURING_ARG_DEFAULTS = {
|
|
2006
|
+
in: nil,
|
|
2007
|
+
out: nil,
|
|
2008
|
+
err: nil,
|
|
2009
|
+
normalize: true,
|
|
2010
|
+
chomp: true,
|
|
2011
|
+
merge: false,
|
|
2012
|
+
chdir: nil,
|
|
2013
|
+
timeout: nil, # Don't set to Git.config.timeout here since it is mutable
|
|
2014
|
+
env: {},
|
|
2015
|
+
raise_on_failure: true
|
|
2016
|
+
}.freeze
|
|
1501
2017
|
|
|
1502
|
-
|
|
1503
|
-
|
|
2018
|
+
STATIC_GLOBAL_OPTS = %w[
|
|
2019
|
+
-c core.quotePath=true
|
|
2020
|
+
-c core.editor=false
|
|
2021
|
+
-c color.ui=false
|
|
2022
|
+
-c color.advice=false
|
|
2023
|
+
-c color.diff=false
|
|
2024
|
+
-c color.grep=false
|
|
2025
|
+
-c color.push=false
|
|
2026
|
+
-c color.remote=false
|
|
2027
|
+
-c color.showBranch=false
|
|
2028
|
+
-c color.status=false
|
|
2029
|
+
-c color.transport=false
|
|
2030
|
+
].freeze
|
|
1504
2031
|
|
|
1505
|
-
|
|
2032
|
+
# Runs a git command and returns the result
|
|
2033
|
+
#
|
|
2034
|
+
# By default, raises {Git::FailedError} if the command exits with a non-zero
|
|
2035
|
+
# status. Pass `raise_on_failure: false` to suppress this behavior.
|
|
2036
|
+
#
|
|
2037
|
+
# @overload command_capturing(*args, **options_hash)
|
|
2038
|
+
# Runs a git command and returns the result
|
|
2039
|
+
#
|
|
2040
|
+
# Args should exclude the 'git' command itself and global options.
|
|
2041
|
+
# Remember to splat the arguments if given as an array.
|
|
2042
|
+
#
|
|
2043
|
+
# @example Run git log
|
|
2044
|
+
# result = command_capturing('log', '--pretty=oneline')
|
|
2045
|
+
# result.stdout #=> "abc123 First commit\ndef456 Second commit\n"
|
|
2046
|
+
#
|
|
2047
|
+
# @example Using an array of arguments
|
|
2048
|
+
# args = ['log', '--pretty=oneline']
|
|
2049
|
+
# result = command_capturing(*args)
|
|
2050
|
+
#
|
|
2051
|
+
# @example Suppress raising on failure
|
|
2052
|
+
# result = command_capturing('show', 'nonexistent', raise_on_failure: false)
|
|
2053
|
+
# result.status.success? #=> false
|
|
2054
|
+
#
|
|
2055
|
+
# @param args [Array<String>] the command and its arguments
|
|
2056
|
+
#
|
|
2057
|
+
# @param options_hash [Hash] the options to pass to the command
|
|
2058
|
+
#
|
|
2059
|
+
# @option options_hash [IO, nil] :in the IO object to use as stdin for the command, or nil to
|
|
2060
|
+
# inherit the parent process stdin. Must be a real IO object with a file descriptor.
|
|
2061
|
+
#
|
|
2062
|
+
# @option options_hash [IO, String, #write, nil] :out the destination for captured stdout
|
|
2063
|
+
#
|
|
2064
|
+
# @option options_hash [IO, String, #write, nil] :err the destination for captured stderr
|
|
2065
|
+
#
|
|
2066
|
+
# @option options_hash [Boolean, nil] :normalize (true) normalize the output encoding to UTF-8
|
|
2067
|
+
#
|
|
2068
|
+
# @option options_hash [Boolean, nil] :chomp (true) remove trailing newlines from the output
|
|
2069
|
+
#
|
|
2070
|
+
# @option options_hash [Boolean, nil] :merge (false) merge stdout and stderr into a single output
|
|
2071
|
+
#
|
|
2072
|
+
# @option options_hash [String, nil] :chdir the directory to run the command in
|
|
2073
|
+
#
|
|
2074
|
+
# @option options_hash [Hash] :env additional environment variable overrides for this command
|
|
2075
|
+
#
|
|
2076
|
+
# @option options_hash [Boolean, nil] :raise_on_failure (true) whether to raise on non-zero exit
|
|
2077
|
+
#
|
|
2078
|
+
# @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete
|
|
2079
|
+
#
|
|
2080
|
+
# If timeout is nil, the global timeout from {Git::Config} is used.
|
|
2081
|
+
#
|
|
2082
|
+
# If timeout is zero, the timeout will not be enforced.
|
|
2083
|
+
#
|
|
2084
|
+
# If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
|
|
2085
|
+
#
|
|
2086
|
+
# If the command does not respond to SIGKILL, it will hang this method.
|
|
2087
|
+
#
|
|
2088
|
+
# @note Individual command classes (under {Git::Commands}) can selectively
|
|
2089
|
+
# expose `:timeout` and `:env` to their callers by declaring them as
|
|
2090
|
+
# execution options in their Arguments DSL definition and forwarding
|
|
2091
|
+
# them to this method. See {Git::Commands::Clone#call} for an example
|
|
2092
|
+
# of a command that exposes `:timeout`.
|
|
2093
|
+
#
|
|
2094
|
+
# @see Git::CommandLine::Capturing#run
|
|
2095
|
+
#
|
|
2096
|
+
# @see #command_line_capturing
|
|
2097
|
+
#
|
|
2098
|
+
# @return [Git::CommandLineResult] the result of the command
|
|
2099
|
+
#
|
|
2100
|
+
# @raise [ArgumentError] if an unknown option is passed
|
|
2101
|
+
#
|
|
2102
|
+
# @raise [Git::FailedError] if the command failed (when raise_on_failure is true)
|
|
2103
|
+
#
|
|
2104
|
+
# @raise [Git::SignaledError] if the command was signaled
|
|
2105
|
+
#
|
|
2106
|
+
# @raise [Git::TimeoutError] if the command times out
|
|
2107
|
+
#
|
|
2108
|
+
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
|
|
2109
|
+
#
|
|
2110
|
+
# The exception's `result` attribute is a {Git::CommandLineResult} which will
|
|
2111
|
+
# contain the result of the command including the exit status, stdout, and
|
|
2112
|
+
# stderr.
|
|
2113
|
+
#
|
|
2114
|
+
def command_capturing(*, **options_hash)
|
|
2115
|
+
options_hash = COMMAND_CAPTURING_ARG_DEFAULTS.merge(options_hash)
|
|
2116
|
+
options_hash[:timeout] ||= Git.config.timeout
|
|
1506
2117
|
|
|
1507
|
-
|
|
1508
|
-
|
|
2118
|
+
extra_options = options_hash.keys - COMMAND_CAPTURING_ARG_DEFAULTS.keys
|
|
2119
|
+
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
1509
2120
|
|
|
1510
|
-
|
|
2121
|
+
env_overrides = options_hash.delete(:env)
|
|
2122
|
+
raise_on_failure = options_hash.delete(:raise_on_failure)
|
|
2123
|
+
command_line_capturing.run(*, raise_on_failure: raise_on_failure, env: env_overrides, **options_hash)
|
|
1511
2124
|
end
|
|
1512
2125
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
2126
|
+
COMMAND_STREAMING_ARG_DEFAULTS = {
|
|
2127
|
+
in: nil,
|
|
2128
|
+
out: nil,
|
|
2129
|
+
err: nil,
|
|
2130
|
+
chdir: nil,
|
|
2131
|
+
timeout: nil,
|
|
2132
|
+
env: {},
|
|
2133
|
+
raise_on_failure: true
|
|
2134
|
+
}.freeze
|
|
2135
|
+
|
|
2136
|
+
# Runs a git command using the streaming (non-capturing) execution path
|
|
2137
|
+
#
|
|
2138
|
+
# Unlike {#command_capturing}, stdout is NOT buffered in memory. It is
|
|
2139
|
+
# written only to the IO object provided via the `out:` option. Stderr is
|
|
2140
|
+
# captured internally via a StringIO for error diagnostics.
|
|
2141
|
+
#
|
|
2142
|
+
# Use this entry point when you want to stream large output (e.g. blob
|
|
2143
|
+
# content from cat-file) without creating memory pressure.
|
|
2144
|
+
#
|
|
2145
|
+
# @overload command_streaming(*args, **options_hash)
|
|
2146
|
+
#
|
|
2147
|
+
# @param args [Array<String>] the git command and its arguments
|
|
2148
|
+
#
|
|
2149
|
+
# @param options_hash [Hash] the options to pass to the command
|
|
2150
|
+
#
|
|
2151
|
+
# @option options_hash [IO, nil] :in stdin IO object
|
|
2152
|
+
#
|
|
2153
|
+
# @option options_hash [#write, nil] :out destination for streamed stdout
|
|
2154
|
+
#
|
|
2155
|
+
# @option options_hash [#write, nil] :err an optional additional destination to receive stderr output
|
|
2156
|
+
# in real time. Stderr is always captured internally; when `err:` is supplied, writes are teed
|
|
2157
|
+
# to both the internal buffer and this destination. `result.stderr` always reflects the internal capture.
|
|
2158
|
+
#
|
|
2159
|
+
# @option options_hash [String, nil] :chdir the directory to run the command in
|
|
2160
|
+
#
|
|
2161
|
+
# @option options_hash [Hash] :env additional environment variable overrides for this command
|
|
2162
|
+
#
|
|
2163
|
+
# @option options_hash [Boolean, nil] :raise_on_failure (true) whether to raise on non-zero exit
|
|
2164
|
+
#
|
|
2165
|
+
# @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command
|
|
2166
|
+
#
|
|
2167
|
+
# If nil, the global timeout from {Git::Config} is used.
|
|
2168
|
+
#
|
|
2169
|
+
# @return [Git::CommandLineResult] the result of the command
|
|
2170
|
+
#
|
|
2171
|
+
# `result.stdout` will always be `''` — stdout was streamed to `out:`.
|
|
2172
|
+
# `result.stderr` contains any stderr output captured for diagnostics.
|
|
2173
|
+
#
|
|
2174
|
+
# @raise [ArgumentError] if an unknown option is passed
|
|
2175
|
+
#
|
|
2176
|
+
# @raise [Git::FailedError] if the command failed (when raise_on_failure is true)
|
|
2177
|
+
#
|
|
2178
|
+
# @raise [Git::SignaledError] if the command was signaled
|
|
2179
|
+
#
|
|
2180
|
+
# @raise [Git::TimeoutError] if the command times out
|
|
2181
|
+
#
|
|
2182
|
+
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
|
|
2183
|
+
#
|
|
2184
|
+
# @see Git::CommandLine::Streaming#run
|
|
2185
|
+
#
|
|
2186
|
+
# @see #command_line_streaming
|
|
2187
|
+
#
|
|
2188
|
+
def command_streaming(*, **options_hash)
|
|
2189
|
+
options_hash = COMMAND_STREAMING_ARG_DEFAULTS.merge(options_hash)
|
|
2190
|
+
options_hash[:timeout] ||= Git.config.timeout
|
|
1516
2191
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
rescue Git::FailedError => e
|
|
1520
|
-
raise unless e.result.status.exitstatus == 1 && e.result.stderr == ''
|
|
2192
|
+
extra_options = options_hash.keys - COMMAND_STREAMING_ARG_DEFAULTS.keys
|
|
2193
|
+
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
1521
2194
|
|
|
1522
|
-
|
|
1523
|
-
|
|
2195
|
+
env_overrides = options_hash.delete(:env)
|
|
2196
|
+
raise_on_failure = options_hash.delete(:raise_on_failure)
|
|
2197
|
+
command_line_streaming.run(*, raise_on_failure: raise_on_failure, env: env_overrides, **options_hash)
|
|
1524
2198
|
end
|
|
1525
2199
|
|
|
1526
|
-
|
|
1527
|
-
|
|
2200
|
+
private
|
|
2201
|
+
|
|
2202
|
+
def migrate_clean_legacy_options(opts)
|
|
2203
|
+
opts = deprecate_clean_option(opts, :ff, ':ff option is deprecated. Use force: 2 instead.')
|
|
2204
|
+
deprecate_clean_option(opts, :force_force, ':force_force option is deprecated. Use force: 2 instead.')
|
|
1528
2205
|
end
|
|
1529
2206
|
|
|
1530
|
-
def
|
|
1531
|
-
|
|
1532
|
-
end
|
|
1533
|
-
|
|
1534
|
-
FSCK_OPTION_MAP = [
|
|
1535
|
-
{ flag: '--no-progress', type: :static },
|
|
1536
|
-
{ keys: [:unreachable], flag: '--unreachable', type: :boolean },
|
|
1537
|
-
{ keys: [:strict], flag: '--strict', type: :boolean },
|
|
1538
|
-
{ keys: [:connectivity_only], flag: '--connectivity-only', type: :boolean },
|
|
1539
|
-
{ keys: [:root], flag: '--root', type: :boolean },
|
|
1540
|
-
{ keys: [:tags], flag: '--tags', type: :boolean },
|
|
1541
|
-
{ keys: [:cache], flag: '--cache', type: :boolean },
|
|
1542
|
-
{ keys: [:no_reflogs], flag: '--no-reflogs', type: :boolean },
|
|
1543
|
-
{ keys: [:lost_found], flag: '--lost-found', type: :boolean },
|
|
1544
|
-
{ keys: [:dangling], flag: '--dangling', type: :boolean_negatable },
|
|
1545
|
-
{ keys: [:full], flag: '--full', type: :boolean_negatable },
|
|
1546
|
-
{ keys: [:name_objects], flag: '--name-objects', type: :boolean_negatable },
|
|
1547
|
-
{ keys: [:references], flag: '--references', type: :boolean_negatable }
|
|
1548
|
-
].freeze
|
|
2207
|
+
def deprecate_clean_option(opts, key, message)
|
|
2208
|
+
return opts unless opts.key?(key)
|
|
1549
2209
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
# fsck returns non-zero exit status when issues are found:
|
|
1554
|
-
# 1 = errors found, 2 = missing objects, 4 = warnings
|
|
1555
|
-
# We still want to parse the output in these cases
|
|
1556
|
-
output = begin
|
|
1557
|
-
command('fsck', *args)
|
|
1558
|
-
rescue Git::FailedError => e
|
|
1559
|
-
raise unless [1, 2, 4].include?(e.result.status.exitstatus)
|
|
1560
|
-
|
|
1561
|
-
e.result.stdout
|
|
1562
|
-
end
|
|
1563
|
-
parse_fsck_output(output)
|
|
1564
|
-
end
|
|
2210
|
+
opts = opts.dup
|
|
2211
|
+
deprecated_value = opts.delete(key)
|
|
2212
|
+
validate_deprecated_clean_option_value!(key, deprecated_value)
|
|
1565
2213
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
].freeze
|
|
2214
|
+
Git::Deprecation.warn(message)
|
|
2215
|
+
return opts unless deprecated_value
|
|
1569
2216
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
flags = build_args(opts, READ_TREE_OPTION_MAP)
|
|
1573
|
-
command('read-tree', *flags, treeish)
|
|
2217
|
+
opts[:force] = merge_clean_force_option(opts[:force], force_specified: force_option_specified?(opts))
|
|
2218
|
+
opts
|
|
1574
2219
|
end
|
|
1575
2220
|
|
|
1576
|
-
def
|
|
1577
|
-
|
|
2221
|
+
def force_option_specified?(opts)
|
|
2222
|
+
opts.key?(:force) && !opts[:force].nil?
|
|
1578
2223
|
end
|
|
1579
2224
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
{ keys: [:message], flag: '-m', type: :valued_space }
|
|
1583
|
-
].freeze
|
|
1584
|
-
|
|
1585
|
-
def commit_tree(tree, opts = {})
|
|
1586
|
-
opts[:message] ||= "commit tree #{tree}"
|
|
1587
|
-
ArgsBuilder.validate!(opts, COMMIT_TREE_OPTION_MAP)
|
|
1588
|
-
|
|
1589
|
-
flags = build_args(opts, COMMIT_TREE_OPTION_MAP)
|
|
1590
|
-
command('commit-tree', tree, *flags)
|
|
1591
|
-
end
|
|
2225
|
+
def validate_deprecated_clean_option_value!(key, value)
|
|
2226
|
+
return if value.nil? || value == true || value == false
|
|
1592
2227
|
|
|
1593
|
-
|
|
1594
|
-
command('update-ref', ref, commit)
|
|
2228
|
+
raise ArgumentError, "#{key} option only accepts true, false, or nil"
|
|
1595
2229
|
end
|
|
1596
2230
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
{ keys: [:force], flag: '--force', type: :boolean },
|
|
1600
|
-
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1601
|
-
{ keys: [:path_limiter], type: :validate_only }
|
|
1602
|
-
].freeze
|
|
2231
|
+
def merge_clean_force_option(existing_force, force_specified: false)
|
|
2232
|
+
return 2 unless force_specified
|
|
1603
2233
|
|
|
1604
|
-
|
|
1605
|
-
ArgsBuilder.validate!(opts, CHECKOUT_INDEX_OPTION_MAP)
|
|
1606
|
-
args = build_args(opts, CHECKOUT_INDEX_OPTION_MAP)
|
|
2234
|
+
normalized_force = normalize_clean_force_option(existing_force)
|
|
1607
2235
|
|
|
1608
|
-
|
|
1609
|
-
|
|
2236
|
+
case normalized_force
|
|
2237
|
+
when Integer then merge_integer_clean_force_option(normalized_force)
|
|
2238
|
+
when false
|
|
2239
|
+
2
|
|
2240
|
+
else
|
|
2241
|
+
normalized_force
|
|
1610
2242
|
end
|
|
1611
|
-
|
|
1612
|
-
command('checkout-index', *args)
|
|
1613
2243
|
end
|
|
1614
2244
|
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
{ keys: [:remote], flag: '--remote', type: :valued_equals },
|
|
1618
|
-
# These options are used by helpers or handled manually
|
|
1619
|
-
{ keys: [:path], type: :validate_only },
|
|
1620
|
-
{ keys: [:format], type: :validate_only },
|
|
1621
|
-
{ keys: [:add_gzip], type: :validate_only }
|
|
1622
|
-
].freeze
|
|
1623
|
-
|
|
1624
|
-
def archive(sha, file = nil, opts = {})
|
|
1625
|
-
ArgsBuilder.validate!(opts, ARCHIVE_OPTION_MAP)
|
|
1626
|
-
file ||= temp_file_name
|
|
1627
|
-
format, gzip = parse_archive_format_options(opts)
|
|
1628
|
-
|
|
1629
|
-
args = build_args(opts, ARCHIVE_OPTION_MAP)
|
|
1630
|
-
args.unshift("--format=#{format}")
|
|
1631
|
-
args << sha
|
|
1632
|
-
args.push('--', opts[:path]) if opts[:path]
|
|
1633
|
-
|
|
1634
|
-
File.open(file, 'wb') { |f| command('archive', *args, out: f) }
|
|
1635
|
-
apply_gzip(file) if gzip
|
|
2245
|
+
def merge_integer_clean_force_option(normalized_force)
|
|
2246
|
+
return normalized_force if normalized_force < 1
|
|
1636
2247
|
|
|
1637
|
-
|
|
2248
|
+
[normalized_force, 2].max
|
|
1638
2249
|
end
|
|
1639
2250
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
version_parts.fill(0, version_parts.length...3)
|
|
2251
|
+
def normalize_clean_force_option(value)
|
|
2252
|
+
case value
|
|
2253
|
+
when true then 1
|
|
2254
|
+
else value
|
|
2255
|
+
end
|
|
1646
2256
|
end
|
|
1647
2257
|
|
|
1648
|
-
#
|
|
2258
|
+
# Build a result hash from clone options for Git::Base.new
|
|
1649
2259
|
#
|
|
1650
|
-
#
|
|
1651
|
-
#
|
|
2260
|
+
# Parses the clone directory from the git command's stderr output, which
|
|
2261
|
+
# contains either:
|
|
2262
|
+
# Cloning into '<directory>'...
|
|
2263
|
+
# Cloning into bare repository '<directory>'...
|
|
1652
2264
|
#
|
|
1653
|
-
#
|
|
1654
|
-
# lib.compare_version_to(2, 42, 0) #=> 0
|
|
1655
|
-
# lib.compare_version_to(2, 43, 0) #=> -1
|
|
2265
|
+
# @param command_line_result [Git::CommandLineResult] the result of the git clone command
|
|
1656
2266
|
#
|
|
1657
|
-
# @param
|
|
1658
|
-
# @return [Integer] -1 if this version is less than other_version, 0 if equal, or 1 if greater than
|
|
2267
|
+
# @param opts [Hash] execution context options (:log, :git_ssh)
|
|
1659
2268
|
#
|
|
1660
|
-
|
|
1661
|
-
|
|
2269
|
+
# @return [Hash] result hash with directory, log, and git_ssh keys
|
|
2270
|
+
#
|
|
2271
|
+
def build_clone_result(command_line_result, opts)
|
|
2272
|
+
clone_dir, bare = parse_clone_stderr(command_line_result.stderr)
|
|
2273
|
+
result = bare ? { repository: clone_dir } : { working_directory: clone_dir }
|
|
2274
|
+
result[:log] = opts[:log] if opts[:log]
|
|
2275
|
+
result[:git_ssh] = opts[:git_ssh] if opts.key?(:git_ssh)
|
|
2276
|
+
result
|
|
1662
2277
|
end
|
|
1663
2278
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
2279
|
+
# Parse the clone directory and bare status from git clone's stderr output
|
|
2280
|
+
#
|
|
2281
|
+
# Git outputs the directory in an unencoded way (no `core.quotePath` or
|
|
2282
|
+
# similar escaping applies to clone's stderr message). The message format
|
|
2283
|
+
# is always:
|
|
2284
|
+
#
|
|
2285
|
+
# Cloning into '<directory>'...
|
|
2286
|
+
# Cloning into bare repository '<directory>'...
|
|
2287
|
+
#
|
|
2288
|
+
# Because the directory name is not escaped, a name containing the
|
|
2289
|
+
# literal sequence `'...` (single-quote followed by three dots) would
|
|
2290
|
+
# be ambiguous. In practice this is extremely unlikely.
|
|
2291
|
+
#
|
|
2292
|
+
# @param stderr [String] stderr output from git clone
|
|
2293
|
+
#
|
|
2294
|
+
# @return [Array(String, Boolean)] the clone directory and whether it's a bare repository
|
|
2295
|
+
#
|
|
2296
|
+
# @raise [Git::UnexpectedResultError] if the stderr output cannot be parsed
|
|
2297
|
+
#
|
|
2298
|
+
def parse_clone_stderr(stderr)
|
|
2299
|
+
match = stderr.match(/Cloning into (?:(bare repository) )?'(.+)'\.\.\./)
|
|
2300
|
+
raise Git::UnexpectedResultError, "Unable to determine clone directory from: #{stderr}" unless match
|
|
1667
2301
|
|
|
1668
|
-
|
|
1669
|
-
(current_command_version <=> required_command_version) >= 0
|
|
2302
|
+
[match[2], !match[1].nil?]
|
|
1670
2303
|
end
|
|
1671
2304
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
2305
|
+
# Prefixes clone result path values with the chdir directory.
|
|
2306
|
+
#
|
|
2307
|
+
# Mutates the given result hash in place, updating any :working_directory
|
|
2308
|
+
# and :repository entries to be rooted under the provided +chdir+ directory.
|
|
2309
|
+
# If +chdir+ is nil, the hash is left unchanged.
|
|
2310
|
+
#
|
|
2311
|
+
# @param result [Hash] clone result hash containing path information
|
|
2312
|
+
# @param chdir [String, nil] directory under which the repository was cloned
|
|
2313
|
+
# @return [nil]
|
|
2314
|
+
#
|
|
2315
|
+
def prefix_clone_result_paths!(result, chdir)
|
|
2316
|
+
return unless chdir
|
|
1676
2317
|
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
warn "[WARNING] The git gem requires git #{lib.required_command_version.join('.')} or later, " \
|
|
1680
|
-
"but only found #{lib.current_command_version.join('.')}. You should probably upgrade."
|
|
2318
|
+
%i[working_directory repository].each do |key|
|
|
2319
|
+
result[key] = File.join(chdir, result[key]) if result.key?(key)
|
|
1681
2320
|
end
|
|
1682
|
-
true
|
|
1683
2321
|
end
|
|
1684
2322
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
STATIC_GLOBAL_OPTS = %w[
|
|
1696
|
-
-c core.quotePath=true
|
|
1697
|
-
-c color.ui=false
|
|
1698
|
-
-c color.advice=false
|
|
1699
|
-
-c color.diff=false
|
|
1700
|
-
-c color.grep=false
|
|
1701
|
-
-c color.push=false
|
|
1702
|
-
-c color.remote=false
|
|
1703
|
-
-c color.showBranch=false
|
|
1704
|
-
-c color.status=false
|
|
1705
|
-
-c color.transport=false
|
|
1706
|
-
].freeze
|
|
2323
|
+
# Handles the deprecated :path option for Git::Lib#clone.
|
|
2324
|
+
#
|
|
2325
|
+
# If opts contains :path, emits a deprecation warning and migrates the
|
|
2326
|
+
# value to :chdir (unless :chdir is already set). Mutates opts in place.
|
|
2327
|
+
#
|
|
2328
|
+
# @param opts [Hash] clone options, possibly containing :path
|
|
2329
|
+
# @return [nil]
|
|
2330
|
+
#
|
|
2331
|
+
def deprecate_clone_path_option!(opts)
|
|
2332
|
+
return unless opts.key?(:path)
|
|
1707
2333
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
{ keys: [:since], flag: '--since', type: :valued_equals },
|
|
1713
|
-
{ keys: [:until], flag: '--until', type: :valued_equals },
|
|
1714
|
-
{ keys: [:grep], flag: '--grep', type: :valued_equals },
|
|
1715
|
-
{ keys: [:author], flag: '--author', type: :valued_equals },
|
|
1716
|
-
{ keys: [:count], flag: '--max-count', type: :valued_equals },
|
|
1717
|
-
{ keys: [:between], type: :custom, builder: ->(value) { "#{value[0]}..#{value[1]}" if value } }
|
|
1718
|
-
].freeze
|
|
2334
|
+
Git::Deprecation.warn('The :path option for Git::Lib#clone is deprecated, use :chdir instead')
|
|
2335
|
+
path = opts.delete(:path)
|
|
2336
|
+
opts[:chdir] ||= path
|
|
2337
|
+
end
|
|
1719
2338
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
FSCK_ROOT_PATTERN = /\Aroot ([0-9a-f]{40})\z/
|
|
1723
|
-
FSCK_TAGGED_PATTERN = /\Atagged (\w+) ([0-9a-f]{40}) \((.+)\) in ([0-9a-f]{40})\z/
|
|
2339
|
+
def deprecate_clone_recursive_option!(opts)
|
|
2340
|
+
return unless opts.key?(:recursive)
|
|
1724
2341
|
|
|
1725
|
-
|
|
2342
|
+
Git::Deprecation.warn('The :recursive option for Git::Lib#clone is deprecated, use :recurse_submodules instead')
|
|
2343
|
+
opts[:recurse_submodules] = opts.delete(:recursive)
|
|
2344
|
+
end
|
|
1726
2345
|
|
|
1727
|
-
|
|
2346
|
+
def deprecate_clone_remote_option!(opts)
|
|
2347
|
+
return unless opts.key?(:remote)
|
|
1728
2348
|
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
output.each_line { |line| parse_fsck_line(line.strip, result) }
|
|
1732
|
-
Git::FsckResult.new(**result)
|
|
2349
|
+
Git::Deprecation.warn('The :remote option for Git::Lib#clone is deprecated, use :origin instead')
|
|
2350
|
+
opts[:origin] = opts.delete(:remote)
|
|
1733
2351
|
end
|
|
1734
2352
|
|
|
1735
|
-
def
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
parse_fsck_tagged_line(line, result)
|
|
2353
|
+
def deprecate_clone_options!(opts)
|
|
2354
|
+
deprecate_clone_path_option!(opts)
|
|
2355
|
+
deprecate_clone_recursive_option!(opts)
|
|
2356
|
+
deprecate_clone_remote_option!(opts)
|
|
1740
2357
|
end
|
|
1741
2358
|
|
|
1742
|
-
def
|
|
1743
|
-
return unless
|
|
2359
|
+
def deprecate_commit_add_all_option!(opts)
|
|
2360
|
+
return unless opts.key?(:add_all)
|
|
1744
2361
|
|
|
1745
|
-
|
|
2362
|
+
Git::Deprecation.warn('The :add_all option for Git::Lib#commit is deprecated, use :all instead')
|
|
2363
|
+
opts[:all] = opts.delete(:add_all)
|
|
1746
2364
|
end
|
|
1747
2365
|
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
2366
|
+
# Extracts execution context options from clone options.
|
|
2367
|
+
#
|
|
2368
|
+
# @param opts [Hash] clone options
|
|
2369
|
+
# @return [Hash] hash with :log and :git_ssh keys if present
|
|
2370
|
+
#
|
|
2371
|
+
def extract_clone_execution_context_opts(opts)
|
|
2372
|
+
result = {}
|
|
2373
|
+
result[:log] = opts.delete(:log) if opts[:log]
|
|
2374
|
+
result[:git_ssh] = opts.delete(:git_ssh) if opts.key?(:git_ssh)
|
|
2375
|
+
result
|
|
1752
2376
|
end
|
|
1753
2377
|
|
|
1754
|
-
|
|
1755
|
-
|
|
2378
|
+
# Translate legacy merge option names to new interface
|
|
2379
|
+
#
|
|
2380
|
+
# @param opts [Hash] options with possibly legacy keys
|
|
2381
|
+
# @return [Hash] options with new keys
|
|
2382
|
+
#
|
|
2383
|
+
def translate_merge_options(opts)
|
|
2384
|
+
result = opts.dup
|
|
2385
|
+
|
|
2386
|
+
# :message => 'msg' becomes :m => 'msg' (git merge uses -m, not --message)
|
|
2387
|
+
result[:m] = result.delete(:message) if result.key?(:message)
|
|
1756
2388
|
|
|
1757
|
-
result
|
|
2389
|
+
result
|
|
1758
2390
|
end
|
|
1759
2391
|
|
|
1760
|
-
|
|
1761
|
-
|
|
2392
|
+
# Extract name-status data from --raw output lines
|
|
2393
|
+
#
|
|
2394
|
+
# Raw lines have the format:
|
|
2395
|
+
# :old_mode new_mode old_sha new_sha status\tpath
|
|
2396
|
+
# or for renames/copies:
|
|
2397
|
+
# :old_mode new_mode old_sha new_sha Rxx\told_path\tnew_path
|
|
2398
|
+
#
|
|
2399
|
+
# @param output [String] raw diff output
|
|
2400
|
+
#
|
|
2401
|
+
# @return [Hash] mapping of file paths to status tokens
|
|
2402
|
+
#
|
|
2403
|
+
def extract_name_status_from_raw(output)
|
|
2404
|
+
output.split("\n").each_with_object({}) do |line, memo|
|
|
2405
|
+
next unless line.start_with?(':')
|
|
1762
2406
|
|
|
1763
|
-
|
|
2407
|
+
parts = line[1..].split(/\s+/, 5)
|
|
2408
|
+
status_and_paths = parts[4].split("\t")
|
|
2409
|
+
status = status_and_paths[0]
|
|
2410
|
+
path = status_and_paths.length > 2 ? status_and_paths[2] : status_and_paths[1]
|
|
2411
|
+
memo[unescape_quoted_path(path)] = status
|
|
2412
|
+
end
|
|
1764
2413
|
end
|
|
1765
2414
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
2415
|
+
# Extract only the patch text from combined numstat + shortstat + patch output
|
|
2416
|
+
#
|
|
2417
|
+
# When {Git::Commands::Diff} is called with `patch: true, numstat: true, shortstat: true`,
|
|
2418
|
+
# the output contains numstat, shortstat, and patch sections. This method extracts
|
|
2419
|
+
# only the patch portion (starting at "diff --git").
|
|
2420
|
+
#
|
|
2421
|
+
# @param output [String] combined command output
|
|
2422
|
+
#
|
|
2423
|
+
# @return [String] only the patch text
|
|
2424
|
+
#
|
|
2425
|
+
def extract_patch_text(output)
|
|
2426
|
+
match = output.match(/^diff --git /m)
|
|
2427
|
+
match ? output[match.begin(0)..] : output
|
|
1771
2428
|
end
|
|
1772
2429
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2430
|
+
# Extract only the numstat lines from combined numstat + shortstat output
|
|
2431
|
+
#
|
|
2432
|
+
# When {Git::Commands::Diff} is called with `numstat: true, shortstat: true`,
|
|
2433
|
+
# the output contains numstat lines followed by a shortstat summary line. This method
|
|
2434
|
+
# filters out the shortstat line and empty lines, returning only the numstat lines.
|
|
2435
|
+
#
|
|
2436
|
+
# @param output [String] combined command output
|
|
2437
|
+
#
|
|
2438
|
+
# @return [Array<String>] only the numstat lines
|
|
2439
|
+
#
|
|
2440
|
+
def extract_numstat_lines(output)
|
|
2441
|
+
output.split("\n").reject { |l| l.empty? || l.match?(/^\s*\d+\s+files?\s+changed/) }
|
|
1782
2442
|
end
|
|
1783
2443
|
|
|
1784
2444
|
def build_args(opts, option_map)
|
|
1785
2445
|
Git::ArgsBuilder.new(opts, option_map).build
|
|
1786
2446
|
end
|
|
1787
2447
|
|
|
2448
|
+
def validate_tag_options!(opts)
|
|
2449
|
+
needs_message = %i[a annotate s sign u local_user].any? { |k| opts[k] }
|
|
2450
|
+
has_message = opts[:m] || opts[:message]
|
|
2451
|
+
|
|
2452
|
+
return unless needs_message && !has_message
|
|
2453
|
+
|
|
2454
|
+
raise ArgumentError, 'Cannot create an annotated or signed tag without a message.'
|
|
2455
|
+
end
|
|
2456
|
+
|
|
2457
|
+
def delete_tag(name)
|
|
2458
|
+
result = Git::Commands::Tag::Delete.new(self).call(name)
|
|
2459
|
+
raise Git::FailedError, result if result.status.exitstatus.positive?
|
|
2460
|
+
|
|
2461
|
+
result.stdout
|
|
2462
|
+
end
|
|
2463
|
+
|
|
2464
|
+
def create_tag(name, target, opts)
|
|
2465
|
+
Git::Commands::Tag::Create.new(self).call(name, target, **opts).stdout
|
|
2466
|
+
end
|
|
2467
|
+
|
|
1788
2468
|
def initialize_from_base(base_object)
|
|
1789
2469
|
@git_dir = base_object.repo.to_s
|
|
1790
2470
|
@git_index_file = base_object.index&.to_s
|
|
@@ -1799,15 +2479,6 @@ module Git
|
|
|
1799
2479
|
@git_ssh = base_hash.key?(:git_ssh) ? base_hash[:git_ssh] : :use_global_config
|
|
1800
2480
|
end
|
|
1801
2481
|
|
|
1802
|
-
def return_base_opts_from_clone(clone_dir, opts)
|
|
1803
|
-
base_opts = {}
|
|
1804
|
-
base_opts[:repository] = clone_dir if opts[:bare] || opts[:mirror]
|
|
1805
|
-
base_opts[:working_directory] = clone_dir unless opts[:bare] || opts[:mirror]
|
|
1806
|
-
base_opts[:log] = opts[:log] if opts[:log]
|
|
1807
|
-
base_opts[:git_ssh] = opts[:git_ssh] if opts.key?(:git_ssh)
|
|
1808
|
-
base_opts
|
|
1809
|
-
end
|
|
1810
|
-
|
|
1811
2482
|
def process_commit_headers(data)
|
|
1812
2483
|
headers = { 'parent' => [] } # Pre-initialize for multiple parents
|
|
1813
2484
|
each_cat_file_header(data) do |key, value|
|
|
@@ -1820,44 +2491,8 @@ module Git
|
|
|
1820
2491
|
headers
|
|
1821
2492
|
end
|
|
1822
2493
|
|
|
1823
|
-
def parse_branch_line(line, index, all_lines)
|
|
1824
|
-
match_data = match_branch_line(line, index, all_lines)
|
|
1825
|
-
|
|
1826
|
-
return nil if match_data[:not_a_branch] || match_data[:detached_ref]
|
|
1827
|
-
|
|
1828
|
-
format_branch_data(match_data)
|
|
1829
|
-
end
|
|
1830
|
-
|
|
1831
|
-
def match_branch_line(line, index, all_lines)
|
|
1832
|
-
match_data = line.match(BRANCH_LINE_REGEXP)
|
|
1833
|
-
raise Git::UnexpectedResultError, unexpected_branch_line_error(all_lines, line, index) unless match_data
|
|
1834
|
-
|
|
1835
|
-
match_data
|
|
1836
|
-
end
|
|
1837
|
-
|
|
1838
|
-
def format_branch_data(match_data)
|
|
1839
|
-
[
|
|
1840
|
-
match_data[:refname],
|
|
1841
|
-
!match_data[:current].nil?,
|
|
1842
|
-
!match_data[:worktree].nil?,
|
|
1843
|
-
match_data[:symref]
|
|
1844
|
-
]
|
|
1845
|
-
end
|
|
1846
|
-
|
|
1847
|
-
def unexpected_branch_line_error(lines, line, index)
|
|
1848
|
-
<<~ERROR
|
|
1849
|
-
Unexpected line in output from `git branch -a`, line #{index + 1}
|
|
1850
|
-
|
|
1851
|
-
Full output:
|
|
1852
|
-
#{lines.join("\n ")}
|
|
1853
|
-
|
|
1854
|
-
Line #{index + 1}:
|
|
1855
|
-
"#{line}"
|
|
1856
|
-
ERROR
|
|
1857
|
-
end
|
|
1858
|
-
|
|
1859
2494
|
def get_branch_state(branch_name)
|
|
1860
|
-
|
|
2495
|
+
Git::Commands::RevParse.new(self).call(branch_name, verify: true, quiet: true)
|
|
1861
2496
|
:active
|
|
1862
2497
|
rescue Git::FailedError => e
|
|
1863
2498
|
# An exit status of 1 with empty stderr from `rev-parse --verify`
|
|
@@ -1867,23 +2502,14 @@ module Git
|
|
|
1867
2502
|
:unborn
|
|
1868
2503
|
end
|
|
1869
2504
|
|
|
1870
|
-
def
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
|
|
1875
|
-
|
|
1876
|
-
[] # Return an empty array for "no matches found"
|
|
2505
|
+
def normalize_grep_opts(opts)
|
|
2506
|
+
opts = opts.dup
|
|
2507
|
+
opts[:pathspec] = opts.delete(:path_limiter) if opts.key?(:path_limiter)
|
|
2508
|
+
opts
|
|
1877
2509
|
end
|
|
1878
2510
|
|
|
1879
|
-
def parse_grep_output(
|
|
1880
|
-
|
|
1881
|
-
match = line.match(/\A(.*?):(\d+):(.*)/)
|
|
1882
|
-
next unless match
|
|
1883
|
-
|
|
1884
|
-
_full, filename, line_num, text = match.to_a
|
|
1885
|
-
hsh[filename] << [line_num.to_i, text]
|
|
1886
|
-
end
|
|
2511
|
+
def parse_grep_output(output)
|
|
2512
|
+
Git::Parsers::Grep.parse(output)
|
|
1887
2513
|
end
|
|
1888
2514
|
|
|
1889
2515
|
def parse_diff_stats_output(lines)
|
|
@@ -1908,6 +2534,18 @@ module Git
|
|
|
1908
2534
|
parts
|
|
1909
2535
|
end
|
|
1910
2536
|
|
|
2537
|
+
def parse_raw_diff_output(stdout)
|
|
2538
|
+
stdout.split("\n").each_with_object({}) do |line, memo|
|
|
2539
|
+
info, file = split_status_line(line)
|
|
2540
|
+
mode_src, mode_dest, sha_src, sha_dest, type = info.split
|
|
2541
|
+
memo[file] = {
|
|
2542
|
+
mode_index: mode_dest, mode_repo: mode_src.to_s[1, 7],
|
|
2543
|
+
path: file, sha_repo: sha_src, sha_index: sha_dest,
|
|
2544
|
+
type: type
|
|
2545
|
+
}
|
|
2546
|
+
end
|
|
2547
|
+
end
|
|
2548
|
+
|
|
1911
2549
|
def build_final_stats_hash(file_stats)
|
|
1912
2550
|
{
|
|
1913
2551
|
total: build_total_stats(file_stats),
|
|
@@ -1953,45 +2591,48 @@ module Git
|
|
|
1953
2591
|
[type, name, value]
|
|
1954
2592
|
end
|
|
1955
2593
|
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2594
|
+
# Convert a StashInfo to the legacy [index, message] format
|
|
2595
|
+
#
|
|
2596
|
+
# The legacy format strips the "WIP on <branch>:" or "On <branch>:" prefix
|
|
2597
|
+
# from the message and returns only the suffix.
|
|
2598
|
+
#
|
|
2599
|
+
# @param info [Git::StashInfo] the stash info object
|
|
2600
|
+
# @return [Array(Integer, String)] `[index, message]` pair with prefix stripped
|
|
2601
|
+
#
|
|
2602
|
+
# @api private
|
|
2603
|
+
#
|
|
2604
|
+
def stash_info_to_legacy(info, index = info.index)
|
|
2605
|
+
full_message = info.message
|
|
1965
2606
|
match_data = full_message.match(/^[^:]+:(.*)$/)
|
|
1966
2607
|
message = match_data ? match_data[1] : full_message
|
|
1967
2608
|
|
|
1968
2609
|
[index, message.strip]
|
|
1969
2610
|
end
|
|
1970
2611
|
|
|
1971
|
-
#
|
|
2612
|
+
# Streams the staged content of a file at a given index stage to an IO object
|
|
2613
|
+
#
|
|
2614
|
+
# Uses the streaming execution path so content is written directly to `out_io`
|
|
2615
|
+
# without being buffered in memory.
|
|
2616
|
+
#
|
|
2617
|
+
# @api private
|
|
1972
2618
|
#
|
|
1973
2619
|
# @param path [String] the path to the file in the index
|
|
1974
2620
|
#
|
|
1975
|
-
# @param stage [Integer] the stage
|
|
2621
|
+
# @param stage [Integer] the index stage to read (e.g., `1` ancestor, `2` ours, `3` theirs)
|
|
1976
2622
|
#
|
|
1977
|
-
# @param out_io [IO] the IO object to
|
|
2623
|
+
# @param out_io [IO] the `IO` object to stream the staged content into
|
|
1978
2624
|
#
|
|
1979
|
-
# @return [IO]
|
|
2625
|
+
# @return [IO] `out_io`, as passed in
|
|
2626
|
+
#
|
|
2627
|
+
# @raise [Git::FailedError] if the object does not exist or git exits non-zero
|
|
2628
|
+
#
|
|
2629
|
+
# @raise [Git::TimeoutError] if the command exceeds the configured timeout
|
|
1980
2630
|
#
|
|
1981
2631
|
def write_staged_content(path, stage, out_io)
|
|
1982
|
-
|
|
2632
|
+
Git::Commands::Show.new(self).call(":#{stage}:#{path}", out: out_io)
|
|
1983
2633
|
out_io
|
|
1984
2634
|
end
|
|
1985
2635
|
|
|
1986
|
-
def validate_tag_options!(opts)
|
|
1987
|
-
is_annotated = opts[:a] || opts[:annotate]
|
|
1988
|
-
has_message = opts[:m] || opts[:message]
|
|
1989
|
-
|
|
1990
|
-
return unless is_annotated && !has_message
|
|
1991
|
-
|
|
1992
|
-
raise ArgumentError, 'Cannot create an annotated tag without a message.'
|
|
1993
|
-
end
|
|
1994
|
-
|
|
1995
2636
|
def normalize_push_args(remote, branch, opts)
|
|
1996
2637
|
if branch.is_a?(Hash)
|
|
1997
2638
|
opts = branch
|
|
@@ -2007,15 +2648,22 @@ module Git
|
|
|
2007
2648
|
[remote, branch, opts]
|
|
2008
2649
|
end
|
|
2009
2650
|
|
|
2010
|
-
def
|
|
2011
|
-
|
|
2012
|
-
|
|
2651
|
+
def validate_push_args!(remote, branch, opts)
|
|
2652
|
+
assert_valid_opts(opts, PUSH_ALLOWED_OPTS)
|
|
2653
|
+
raise ArgumentError, 'remote is required if branch is specified' if !remote && branch
|
|
2654
|
+
end
|
|
2655
|
+
|
|
2656
|
+
def push_refs(remote, branch, opts)
|
|
2657
|
+
positionals = [remote, branch].compact
|
|
2658
|
+
Git::Commands::Push.new(self).call(*positionals, **opts.except(:tags))
|
|
2659
|
+
end
|
|
2660
|
+
|
|
2661
|
+
def push_tags_separately?(opts)
|
|
2662
|
+
opts[:tags] && !opts[:mirror]
|
|
2663
|
+
end
|
|
2013
2664
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
args << remote if remote
|
|
2017
|
-
args << branch if branch
|
|
2018
|
-
args
|
|
2665
|
+
def push_tags(remote, opts)
|
|
2666
|
+
Git::Commands::Push.new(self).call(*[remote].compact, **opts)
|
|
2019
2667
|
end
|
|
2020
2668
|
|
|
2021
2669
|
def temp_file_name
|
|
@@ -2037,16 +2685,6 @@ module Git
|
|
|
2037
2685
|
Zlib::GzipWriter.open(file) { |gz| gz.write(file_content) }
|
|
2038
2686
|
end
|
|
2039
2687
|
|
|
2040
|
-
def command_lines(cmd, *opts, chdir: nil)
|
|
2041
|
-
cmd_op = command(cmd, *opts, chdir: chdir)
|
|
2042
|
-
op = if cmd_op.encoding.name == 'UTF-8'
|
|
2043
|
-
cmd_op
|
|
2044
|
-
else
|
|
2045
|
-
cmd_op.encode('UTF-8', 'binary', invalid: :replace, undef: :replace)
|
|
2046
|
-
end
|
|
2047
|
-
op.split("\n")
|
|
2048
|
-
end
|
|
2049
|
-
|
|
2050
2688
|
# Returns a hash of environment variable overrides for git commands
|
|
2051
2689
|
#
|
|
2052
2690
|
# This method builds a hash of environment variables that control git's behavior,
|
|
@@ -2074,7 +2712,7 @@ module Git
|
|
|
2074
2712
|
# @example Unsetting an environment variable (used by worktree_command_line)
|
|
2075
2713
|
# env_overrides('GIT_INDEX_FILE' => nil)
|
|
2076
2714
|
# # => { 'GIT_DIR' => '/path/to/.git', 'GIT_WORK_TREE' => '/path/to/worktree',
|
|
2077
|
-
# # 'GIT_INDEX_FILE' => nil, 'GIT_SSH' => <git_ssh_value>, 'LC_ALL' =>
|
|
2715
|
+
# # 'GIT_INDEX_FILE' => nil, 'GIT_SSH' => <git_ssh_value>, 'LC_ALL' => 'en_US.UTF-8' }
|
|
2078
2716
|
# # When passed to Process.spawn, GIT_INDEX_FILE will be unset in the environment
|
|
2079
2717
|
#
|
|
2080
2718
|
# @see https://ruby-doc.org/core/Process.html#method-c-spawn Process.spawn
|
|
@@ -2087,67 +2725,11 @@ module Git
|
|
|
2087
2725
|
'GIT_WORK_TREE' => @git_work_dir,
|
|
2088
2726
|
'GIT_INDEX_FILE' => @git_index_file,
|
|
2089
2727
|
'GIT_SSH' => resolved_git_ssh,
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
# ("* (HEAD losgelöst bei origin/25.1)") broke branch parsing.
|
|
2093
|
-
#
|
|
2094
|
-
# The pin has two halves, and both are load-bearing here:
|
|
2095
|
-
#
|
|
2096
|
-
# - Messages: keeps git's human-readable text in English. `BRANCH_LINE_REGEXP`
|
|
2097
|
-
# matches the literal English strings "(HEAD detached at ...)" and
|
|
2098
|
-
# "(not a branch)" in `git branch -a` output, which is the exact issue #753
|
|
2099
|
-
# failure mode.
|
|
2100
|
-
# - Ctype: makes git's regex engine match *characters* rather than *bytes*.
|
|
2101
|
-
# Under a C ctype, `grep` and `log --grep` silently return wrong answers —
|
|
2102
|
-
# with exit status zero — for any pattern whose metacharacters span
|
|
2103
|
-
# non-ASCII text.
|
|
2104
|
-
#
|
|
2105
|
-
# So do not "simplify" this to `C`: that yields English messages and a broken
|
|
2106
|
-
# ctype, which is the worst of both. A pinned locale the host does not have
|
|
2107
|
-
# degrades to that same C ctype, which is why each branch below has to name a
|
|
2108
|
-
# locale that actually exists on the platform it applies to:
|
|
2109
|
-
#
|
|
2110
|
-
# - Non-Darwin: `C.UTF-8`. Stock Debian, Ubuntu, and RHEL images generate no
|
|
2111
|
-
# `en_US.UTF-8`, so pinning it there produced exactly the silent breakage
|
|
2112
|
-
# above. (musl ignores the locale name for ctype, so Alpine is UTF-8 either
|
|
2113
|
-
# way.)
|
|
2114
|
-
# - Darwin: `en_US.UTF-8`, which every macOS release ships. `C.UTF-8` did not
|
|
2115
|
-
# arrive until macOS 15, so macOS 11–14 — including Intel Macs that are
|
|
2116
|
-
# hardware-capped below 15 — would break under it. Delete this branch once
|
|
2117
|
-
# macOS 14 and earlier are out of support.
|
|
2118
|
-
#
|
|
2119
|
-
# Windows takes the non-Darwin branch, where the value makes no difference:
|
|
2120
|
-
# Git for Windows folds case and runs PCRE in UTF mode under every value, and
|
|
2121
|
-
# matches bytes for `.` and POSIX classes under every value — measured on git
|
|
2122
|
-
# 2.55.0 against `en_US.UTF-8`, `C.UTF-8`, `C`, and no pin at all.
|
|
2123
|
-
#
|
|
2124
|
-
# RHEL 7 (glibc 2.17) has neither locale and gets a C ctype whatever is pinned.
|
|
2125
|
-
# It is EOL, and there is deliberately no public override for this value.
|
|
2126
|
-
'LC_ALL' => darwin_platform? ? 'en_US.UTF-8' : 'C.UTF-8'
|
|
2728
|
+
'GIT_EDITOR' => 'true', # Use a no-op editor so Git skips interactive editing but continues
|
|
2729
|
+
'LC_ALL' => 'en_US.UTF-8'
|
|
2127
2730
|
}.merge(additional_overrides)
|
|
2128
2731
|
end
|
|
2129
2732
|
|
|
2130
|
-
# Whether this process is running on macOS
|
|
2131
|
-
#
|
|
2132
|
-
# Checks `RUBY_DESCRIPTION` as well as `RUBY_PLATFORM` because JRuby reports
|
|
2133
|
-
# `RUBY_PLATFORM` as `"java"` on every host and records the real platform only in
|
|
2134
|
-
# `RUBY_DESCRIPTION` (as, for example, `"... [arm64-darwin]"`). Testing
|
|
2135
|
-
# `RUBY_PLATFORM` alone would put JRuby on macOS onto the non-Darwin branch, which is
|
|
2136
|
-
# the one platform pairing that branch must not be given. This is the same detection
|
|
2137
|
-
# the test suite uses for Windows, and for the same reason.
|
|
2138
|
-
#
|
|
2139
|
-
# Test the platform plainly rather than sniffing the Darwin version: `RUBY_PLATFORM`
|
|
2140
|
-
# records the version Ruby was *built* against, so a Ruby built on macOS 14 still
|
|
2141
|
-
# reports `darwin23` when run on macOS 15.
|
|
2142
|
-
#
|
|
2143
|
-
# @return [Boolean] true if this process is running on macOS
|
|
2144
|
-
#
|
|
2145
|
-
# @api private
|
|
2146
|
-
#
|
|
2147
|
-
def darwin_platform?
|
|
2148
|
-
RUBY_PLATFORM.include?('darwin') || RUBY_DESCRIPTION.include?('darwin')
|
|
2149
|
-
end
|
|
2150
|
-
|
|
2151
2733
|
# Resolve the git_ssh value to use for this instance
|
|
2152
2734
|
#
|
|
2153
2735
|
# @return [String, nil] the resolved git_ssh value
|
|
@@ -2171,154 +2753,85 @@ module Git
|
|
|
2171
2753
|
end
|
|
2172
2754
|
end
|
|
2173
2755
|
|
|
2174
|
-
|
|
2175
|
-
@command_line ||=
|
|
2176
|
-
Git::CommandLine.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
|
|
2177
|
-
end
|
|
2178
|
-
|
|
2179
|
-
# Returns a command line instance without GIT_INDEX_FILE for worktree commands
|
|
2180
|
-
#
|
|
2181
|
-
# Git worktrees manage their own index files and setting GIT_INDEX_FILE
|
|
2182
|
-
# causes corruption of both the main worktree and new worktree indexes.
|
|
2183
|
-
#
|
|
2184
|
-
# @return [Git::CommandLine]
|
|
2185
|
-
# @api private
|
|
2186
|
-
#
|
|
2187
|
-
def worktree_command_line
|
|
2188
|
-
@worktree_command_line ||=
|
|
2189
|
-
Git::CommandLine.new(env_overrides('GIT_INDEX_FILE' => nil), Git::Base.config.binary_path, global_opts,
|
|
2190
|
-
@logger)
|
|
2191
|
-
end
|
|
2192
|
-
|
|
2193
|
-
# @overload worktree_command(*args, **options_hash)
|
|
2194
|
-
# Runs a git worktree command and returns the output
|
|
2756
|
+
# Returns the {Git::CommandLine::Capturing} instance used for capturing execution
|
|
2195
2757
|
#
|
|
2196
|
-
#
|
|
2197
|
-
#
|
|
2758
|
+
# Memoized factory for the capturing execution path. Instantiates
|
|
2759
|
+
# {Git::CommandLine::Capturing} with the current environment, binary path,
|
|
2760
|
+
# global options, and logger.
|
|
2198
2761
|
#
|
|
2199
|
-
#
|
|
2200
|
-
# @param options_hash [Hash] the options to pass to the command
|
|
2201
|
-
#
|
|
2202
|
-
# @return [String] the command's stdout
|
|
2762
|
+
# @return [Git::CommandLine::Capturing]
|
|
2203
2763
|
#
|
|
2204
|
-
# @see #
|
|
2205
|
-
#
|
|
2206
|
-
# @api private
|
|
2764
|
+
# @see Git::CommandLine::Capturing#run
|
|
2207
2765
|
#
|
|
2208
|
-
def
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
extra_options = options_hash.keys - COMMAND_ARG_DEFAULTS.keys
|
|
2213
|
-
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
2214
|
-
|
|
2215
|
-
result = worktree_command_line.run(*, **options_hash)
|
|
2216
|
-
result.stdout
|
|
2766
|
+
def command_line_capturing
|
|
2767
|
+
@command_line_capturing ||=
|
|
2768
|
+
Git::CommandLine::Capturing.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
|
|
2217
2769
|
end
|
|
2218
2770
|
|
|
2219
|
-
#
|
|
2220
|
-
#
|
|
2221
|
-
# Additional args are passed to the command line. They should exclude the 'git'
|
|
2222
|
-
# command itself and global options. Remember to splat the the arguments if given
|
|
2223
|
-
# as an array.
|
|
2224
|
-
#
|
|
2225
|
-
# For example, to run `git log --pretty=oneline`, you would create the array
|
|
2226
|
-
# `args = ['log', '--pretty=oneline']` and call `command(*args)`.
|
|
2227
|
-
#
|
|
2228
|
-
# @param options_hash [Hash] the options to pass to the command
|
|
2229
|
-
# @option options_hash [IO, String, #write, nil] :out the destination for captured stdout
|
|
2230
|
-
# @option options_hash [IO, String, #write, nil] :err the destination for captured stderr
|
|
2231
|
-
# @option options_hash [Boolean] :normalize true to normalize the output encoding to UTF-8
|
|
2232
|
-
# @option options_hash [Boolean] :chomp true to remove trailing newlines from the output
|
|
2233
|
-
# @option options_hash [Boolean] :merge true to merge stdout and stderr into a single output
|
|
2234
|
-
# @option options_hash [String, nil] :chdir the directory to run the command in
|
|
2235
|
-
# @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete
|
|
2236
|
-
#
|
|
2237
|
-
# If timeout is nil, the global timeout from {Git::Config} is used.
|
|
2238
|
-
#
|
|
2239
|
-
# If timeout is zero, the timeout will not be enforced.
|
|
2240
|
-
#
|
|
2241
|
-
# If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
|
|
2242
|
-
#
|
|
2243
|
-
# If the command does not respond to SIGKILL, it will hang this method.
|
|
2244
|
-
#
|
|
2245
|
-
# @see Git::CommandLine#run
|
|
2246
|
-
#
|
|
2247
|
-
# @return [String] the command's stdout (or merged stdout and stderr if `merge`
|
|
2248
|
-
# is true)
|
|
2249
|
-
#
|
|
2250
|
-
# @raise [ArgumentError] if an unknown option is passed
|
|
2771
|
+
# Returns the {Git::CommandLine::Streaming} instance used for streaming execution
|
|
2251
2772
|
#
|
|
2252
|
-
#
|
|
2773
|
+
# Memoized factory for the streaming execution path. Instantiates
|
|
2774
|
+
# {Git::CommandLine::Streaming} with the current environment, binary path,
|
|
2775
|
+
# global options, and logger.
|
|
2253
2776
|
#
|
|
2254
|
-
# @
|
|
2255
|
-
#
|
|
2256
|
-
# @raise [Git::TimeoutError] if the command times out
|
|
2257
|
-
#
|
|
2258
|
-
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
|
|
2259
|
-
#
|
|
2260
|
-
# The exception's `result` attribute is a {Git::CommandLineResult} which will
|
|
2261
|
-
# contain the result of the command including the exit status, stdout, and
|
|
2262
|
-
# stderr.
|
|
2777
|
+
# @return [Git::CommandLine::Streaming]
|
|
2263
2778
|
#
|
|
2264
|
-
# @
|
|
2779
|
+
# @see Git::CommandLine::Streaming#run
|
|
2265
2780
|
#
|
|
2266
|
-
def
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
extra_options = options_hash.keys - COMMAND_ARG_DEFAULTS.keys
|
|
2271
|
-
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
2272
|
-
|
|
2273
|
-
result = command_line.run(*, **options_hash)
|
|
2274
|
-
result.stdout
|
|
2781
|
+
def command_line_streaming
|
|
2782
|
+
@command_line_streaming ||=
|
|
2783
|
+
Git::CommandLine::Streaming.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
|
|
2275
2784
|
end
|
|
2276
2785
|
|
|
2277
|
-
#
|
|
2786
|
+
# Validates the :count option for log commands.
|
|
2278
2787
|
#
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
# @return [Hash] the diff as Hash
|
|
2282
|
-
def diff_as_hash(diff_command, opts = [])
|
|
2283
|
-
# update index before diffing to avoid spurious diffs
|
|
2284
|
-
command('status')
|
|
2285
|
-
command_lines(diff_command, *opts).each_with_object({}) do |line, memo|
|
|
2286
|
-
info, file = split_status_line(line)
|
|
2287
|
-
mode_src, mode_dest, sha_src, sha_dest, type = info.split
|
|
2788
|
+
def validate_log_count_option!(opts)
|
|
2789
|
+
return unless opts[:count] && !opts[:count].is_a?(Integer)
|
|
2288
2790
|
|
|
2289
|
-
|
|
2290
|
-
mode_index: mode_dest, mode_repo: mode_src.to_s[1, 7],
|
|
2291
|
-
path: file, sha_repo: sha_src, sha_index: sha_dest,
|
|
2292
|
-
type: type
|
|
2293
|
-
}
|
|
2294
|
-
end
|
|
2791
|
+
raise ArgumentError, "The log count option must be an Integer but was #{opts[:count].inspect}"
|
|
2295
2792
|
end
|
|
2296
2793
|
|
|
2297
|
-
#
|
|
2794
|
+
# Builds the positional revision range argument(s) from opts for Git::Commands::Log
|
|
2298
2795
|
#
|
|
2299
|
-
# @param [Hash]
|
|
2300
|
-
# @return [Array]
|
|
2301
|
-
def
|
|
2302
|
-
if opts[:
|
|
2303
|
-
|
|
2796
|
+
# @param opts [Hash]
|
|
2797
|
+
# @return [Array<String>] zero or one element array with the revision range expression
|
|
2798
|
+
def log_revision_range_args(opts)
|
|
2799
|
+
if opts[:between]
|
|
2800
|
+
["#{opts[:between][0]}..#{opts[:between][1]}"]
|
|
2801
|
+
elsif opts[:object].is_a?(String)
|
|
2802
|
+
[opts[:object]]
|
|
2803
|
+
else
|
|
2804
|
+
[]
|
|
2304
2805
|
end
|
|
2305
|
-
|
|
2306
|
-
build_args(opts, LOG_OPTION_MAP)
|
|
2307
2806
|
end
|
|
2308
2807
|
|
|
2309
|
-
#
|
|
2808
|
+
# Builds the common keyword options for Git::Commands::Log from opts
|
|
2310
2809
|
#
|
|
2311
|
-
# @param [Hash]
|
|
2312
|
-
# @
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2810
|
+
# @param opts [Hash]
|
|
2811
|
+
# @param extra [Hash] additional options to merge in (caller-specific)
|
|
2812
|
+
# @return [Hash] keyword arguments for Git::Commands::Log#call
|
|
2813
|
+
def log_base_call_options(opts, extra = {})
|
|
2814
|
+
{
|
|
2815
|
+
all: opts[:all],
|
|
2816
|
+
cherry: opts[:cherry],
|
|
2817
|
+
since: opts[:since],
|
|
2818
|
+
until: opts[:until],
|
|
2819
|
+
grep: opts[:grep],
|
|
2820
|
+
author: opts[:author],
|
|
2821
|
+
max_count: opts[:count],
|
|
2822
|
+
path: opts[:path_limiter] ? Array(opts[:path_limiter]) : nil
|
|
2823
|
+
}.merge(extra).compact
|
|
2824
|
+
end
|
|
2825
|
+
|
|
2826
|
+
def run_log_command(revision_range_args, call_opts)
|
|
2827
|
+
log_or_empty_on_unborn do
|
|
2828
|
+
result = Git::Commands::Log.new(self).call(
|
|
2829
|
+
*revision_range_args,
|
|
2830
|
+
no_color: true, pretty: 'raw',
|
|
2831
|
+
**call_opts
|
|
2832
|
+
)
|
|
2833
|
+
process_commit_log_data(result.stdout.split("\n"))
|
|
2320
2834
|
end
|
|
2321
|
-
arr_opts
|
|
2322
2835
|
end
|
|
2323
2836
|
|
|
2324
2837
|
def log_or_empty_on_unborn
|
|
@@ -2329,5 +2842,14 @@ module Git
|
|
|
2329
2842
|
|
|
2330
2843
|
[]
|
|
2331
2844
|
end
|
|
2845
|
+
|
|
2846
|
+
def normalize_commit_tree_opts(opts, tree)
|
|
2847
|
+
opts.dup.tap do |actual_opts|
|
|
2848
|
+
actual_opts[:p] = actual_opts.delete(:parents) if actual_opts.key?(:parents)
|
|
2849
|
+
actual_opts[:p] = actual_opts.delete(:parent) if actual_opts.key?(:parent)
|
|
2850
|
+
actual_opts[:m] = actual_opts.delete(:message) if actual_opts.key?(:message)
|
|
2851
|
+
actual_opts[:m] = "commit tree #{tree}" if actual_opts[:m].nil?
|
|
2852
|
+
end
|
|
2853
|
+
end
|
|
2332
2854
|
end
|
|
2333
2855
|
end
|