git 4.3.2 → 5.0.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -2
- 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 -40
- data/CONTRIBUTING.md +694 -53
- data/README.md +17 -5
- 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 +486 -410
- data/lib/git/branch.rb +380 -43
- 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 -1028
- 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 +224 -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 +114 -18
- 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'
|
|
@@ -11,12 +18,15 @@ require 'process_executer'
|
|
|
11
18
|
require 'stringio'
|
|
12
19
|
require 'tempfile'
|
|
13
20
|
require 'zlib'
|
|
14
|
-
require 'open3'
|
|
15
21
|
|
|
16
22
|
module Git
|
|
17
23
|
# Internal git operations
|
|
18
24
|
# @api private
|
|
19
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
|
+
|
|
20
30
|
# The path to the Git working copy. The default is '"./.git"'.
|
|
21
31
|
#
|
|
22
32
|
# @return [Pathname] the path to the Git working copy.
|
|
@@ -75,52 +85,29 @@ module Git
|
|
|
75
85
|
end
|
|
76
86
|
end
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
#
|
|
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`)
|
|
84
101
|
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
# :initial_branch
|
|
102
|
+
# @option opts [String, nil] :repository (nil) deprecated — use `:separate_git_dir` instead
|
|
103
|
+
#
|
|
104
|
+
# @return [String] the command output
|
|
89
105
|
#
|
|
90
106
|
def init(opts = {})
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
CLONE_OPTION_MAP = [
|
|
96
|
-
{ keys: [:bare], flag: '--bare', type: :boolean },
|
|
97
|
-
{ keys: [:recursive], flag: '--recursive', type: :boolean },
|
|
98
|
-
{ keys: [:mirror], flag: '--mirror', type: :boolean },
|
|
99
|
-
{ keys: [:branch], flag: '--branch', type: :valued_space },
|
|
100
|
-
{ keys: [:filter], flag: '--filter', type: :valued_space },
|
|
101
|
-
{ keys: %i[remote origin], flag: '--origin', type: :valued_space },
|
|
102
|
-
{ keys: [:config], flag: '--config', type: :repeatable_valued_space },
|
|
103
|
-
{
|
|
104
|
-
keys: [:single_branch],
|
|
105
|
-
type: :custom,
|
|
106
|
-
validator: ->(value) { [nil, true, false].include?(value) },
|
|
107
|
-
builder: lambda do |value|
|
|
108
|
-
case value
|
|
109
|
-
when true
|
|
110
|
-
['--single-branch']
|
|
111
|
-
when false
|
|
112
|
-
['--no-single-branch']
|
|
113
|
-
else
|
|
114
|
-
[]
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
keys: [:depth],
|
|
120
|
-
type: :custom,
|
|
121
|
-
builder: ->(value) { ['--depth', value.to_i] if value }
|
|
122
|
-
}
|
|
123
|
-
].freeze
|
|
107
|
+
opts = opts.dup
|
|
108
|
+
opts[:separate_git_dir] ||= opts.delete(:repository)
|
|
109
|
+
Git::Commands::Init.new(self).call(**opts).stdout
|
|
110
|
+
end
|
|
124
111
|
|
|
125
112
|
# Clones a repository into a newly created directory
|
|
126
113
|
#
|
|
@@ -133,28 +120,42 @@ module Git
|
|
|
133
120
|
#
|
|
134
121
|
# @param [Hash] opts the options for this command
|
|
135
122
|
#
|
|
136
|
-
# @option opts [Boolean] :bare (
|
|
123
|
+
# @option opts [Boolean, nil] :bare (nil) if true, clone as a bare repository
|
|
137
124
|
#
|
|
138
|
-
# @option opts [String] :branch the branch to checkout
|
|
125
|
+
# @option opts [String, nil] :branch (nil) the branch to checkout
|
|
139
126
|
#
|
|
140
|
-
# @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
|
|
141
128
|
#
|
|
142
|
-
# @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
|
|
143
130
|
#
|
|
144
|
-
# @option opts [String] :filter specify partial clone
|
|
131
|
+
# @option opts [String, nil] :filter (nil) specify partial clone
|
|
145
132
|
#
|
|
146
|
-
# @option opts [String] :
|
|
133
|
+
# @option opts [String, nil] :git_ssh (nil) SSH command or binary to use for git over SSH
|
|
147
134
|
#
|
|
148
|
-
# @option opts [
|
|
135
|
+
# @option opts [Logger, nil] :log (nil) Logger instance to use for git operations
|
|
149
136
|
#
|
|
150
|
-
# @option opts [
|
|
137
|
+
# @option opts [Boolean, nil] :mirror (nil) set up a mirror of the source repository
|
|
151
138
|
#
|
|
152
|
-
# @option opts [String] :
|
|
139
|
+
# @option opts [String, nil] :origin (nil) the name of the remote
|
|
153
140
|
#
|
|
154
|
-
# @option opts [
|
|
155
|
-
# within, using their default settings
|
|
141
|
+
# @option opts [String, nil] :chdir (nil) run `git clone` from this directory
|
|
156
142
|
#
|
|
157
|
-
#
|
|
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
|
|
158
159
|
# command to complete
|
|
159
160
|
#
|
|
160
161
|
# See {Git::Lib#command} for more information about :timeout
|
|
@@ -163,16 +164,16 @@ module Git
|
|
|
163
164
|
#
|
|
164
165
|
# @todo make this work with SSH password or auth_key
|
|
165
166
|
#
|
|
166
|
-
def clone(repository_url, directory, opts = {})
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
176
177
|
end
|
|
177
178
|
|
|
178
179
|
# Returns the name of the default branch of the given repository
|
|
@@ -182,7 +183,7 @@ module Git
|
|
|
182
183
|
# @return [String] the name of the default branch
|
|
183
184
|
#
|
|
184
185
|
def repository_default_branch(repository)
|
|
185
|
-
output =
|
|
186
|
+
output = Git::Commands::LsRemote.new(self).call(repository, 'HEAD', symref: true).stdout
|
|
186
187
|
|
|
187
188
|
match_data = output.match(%r{^ref: refs/remotes/origin/(?<default_branch>[^\t]+)\trefs/remotes/origin/HEAD$})
|
|
188
189
|
return match_data[:default_branch] if match_data
|
|
@@ -195,29 +196,6 @@ module Git
|
|
|
195
196
|
|
|
196
197
|
## READ COMMANDS ##
|
|
197
198
|
|
|
198
|
-
# The map defining how to translate user options to git command arguments.
|
|
199
|
-
DESCRIBE_OPTION_MAP = [
|
|
200
|
-
{ keys: [:all], flag: '--all', type: :boolean },
|
|
201
|
-
{ keys: [:tags], flag: '--tags', type: :boolean },
|
|
202
|
-
{ keys: [:contains], flag: '--contains', type: :boolean },
|
|
203
|
-
{ keys: [:debug], flag: '--debug', type: :boolean },
|
|
204
|
-
{ keys: [:long], flag: '--long', type: :boolean },
|
|
205
|
-
{ keys: [:always], flag: '--always', type: :boolean },
|
|
206
|
-
{ keys: %i[exact_match exact-match], flag: '--exact-match', type: :boolean },
|
|
207
|
-
{ keys: [:abbrev], flag: '--abbrev', type: :valued_equals },
|
|
208
|
-
{ keys: [:candidates], flag: '--candidates', type: :valued_equals },
|
|
209
|
-
{ keys: [:match], flag: '--match', type: :valued_equals },
|
|
210
|
-
{
|
|
211
|
-
keys: [:dirty],
|
|
212
|
-
type: :custom,
|
|
213
|
-
builder: lambda do |value|
|
|
214
|
-
return '--dirty' if value == true
|
|
215
|
-
|
|
216
|
-
"--dirty=#{value}" if value.is_a?(String)
|
|
217
|
-
end
|
|
218
|
-
}
|
|
219
|
-
].freeze
|
|
220
|
-
|
|
221
199
|
# Finds most recent tag that is reachable from a commit
|
|
222
200
|
#
|
|
223
201
|
# @see https://git-scm.com/docs/git-describe git-describe
|
|
@@ -226,13 +204,13 @@ module Git
|
|
|
226
204
|
#
|
|
227
205
|
# @param opts [Hash] the given options
|
|
228
206
|
#
|
|
229
|
-
# @option opts :all
|
|
230
|
-
# @option opts :tags
|
|
231
|
-
# @option opts :contains
|
|
232
|
-
# @option opts :debug
|
|
233
|
-
# @option opts :long
|
|
234
|
-
# @option opts :always
|
|
235
|
-
# @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
|
|
236
214
|
# @option opts :dirty [true, String]
|
|
237
215
|
# @option opts :abbrev [String]
|
|
238
216
|
# @option opts :candidates [String]
|
|
@@ -245,59 +223,14 @@ module Git
|
|
|
245
223
|
def describe(commit_ish = nil, opts = {})
|
|
246
224
|
assert_args_are_not_options('commit-ish object', commit_ish)
|
|
247
225
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
command('describe', *args)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
# Return the commits that are within the given revision range
|
|
255
|
-
#
|
|
256
|
-
# @see https://git-scm.com/docs/git-log git-log
|
|
257
|
-
#
|
|
258
|
-
# @param opts [Hash] the given options
|
|
259
|
-
#
|
|
260
|
-
# @option opts :count [Integer] the maximum number of commits to return (maps to max-count)
|
|
261
|
-
# @option opts :all [Boolean]
|
|
262
|
-
# @option opts :cherry [Boolean]
|
|
263
|
-
# @option opts :since [String]
|
|
264
|
-
# @option opts :until [String]
|
|
265
|
-
# @option opts :grep [String]
|
|
266
|
-
# @option opts :author [String]
|
|
267
|
-
# @option opts :between [Array<String>] an array of two commit-ish strings to specify a revision range
|
|
268
|
-
#
|
|
269
|
-
# Only :between or :object options can be used, not both.
|
|
270
|
-
#
|
|
271
|
-
# @option opts :object [String] the revision range for the git log command
|
|
272
|
-
#
|
|
273
|
-
# Only :between or :object options can be used, not both.
|
|
274
|
-
#
|
|
275
|
-
# @option opts :path_limiter [String, Pathname, Array<String, Pathname>] only
|
|
276
|
-
# include commits that impact files from the specified paths
|
|
277
|
-
#
|
|
278
|
-
# @return [Array<String>] the log output
|
|
279
|
-
#
|
|
280
|
-
# @raise [ArgumentError] if the resulting revision range is a string starting with a hyphen
|
|
281
|
-
#
|
|
282
|
-
def log_commits(opts = {})
|
|
283
|
-
assert_args_are_not_options('between', opts[:between]&.first)
|
|
284
|
-
assert_args_are_not_options('object', opts[:object])
|
|
285
|
-
|
|
286
|
-
arr_opts = log_common_options(opts)
|
|
287
|
-
|
|
288
|
-
arr_opts << '--pretty=oneline'
|
|
289
|
-
|
|
290
|
-
arr_opts += log_path_options(opts)
|
|
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')
|
|
291
229
|
|
|
292
|
-
|
|
230
|
+
commit_ishes = Array(commit_ish).compact
|
|
231
|
+
Git::Commands::Describe.new(self).call(*commit_ishes, **opts).stdout
|
|
293
232
|
end
|
|
294
233
|
|
|
295
|
-
FULL_LOG_EXTRA_OPTIONS_MAP = [
|
|
296
|
-
{ type: :static, flag: '--pretty=raw' },
|
|
297
|
-
{ keys: [:skip], flag: '--skip', type: :valued_equals },
|
|
298
|
-
{ keys: [:merges], flag: '--merges', type: :boolean }
|
|
299
|
-
].freeze
|
|
300
|
-
|
|
301
234
|
# Return the commits that are within the given revision range
|
|
302
235
|
#
|
|
303
236
|
# @see https://git-scm.com/docs/git-log git-log
|
|
@@ -307,9 +240,9 @@ module Git
|
|
|
307
240
|
# @option opts :count [Integer] the maximum number of commits to return (maps to
|
|
308
241
|
# max-count)
|
|
309
242
|
#
|
|
310
|
-
# @option opts :all
|
|
243
|
+
# @option opts [Boolean, nil] :all (nil) include commits reachable from any ref
|
|
311
244
|
#
|
|
312
|
-
# @option opts :cherry
|
|
245
|
+
# @option opts [Boolean, nil] :cherry (nil) omit commits equivalent to cherry-picked commits
|
|
313
246
|
#
|
|
314
247
|
# @option opts :since [String]
|
|
315
248
|
#
|
|
@@ -353,17 +286,11 @@ module Git
|
|
|
353
286
|
# :object) is a string starting with a hyphen
|
|
354
287
|
#
|
|
355
288
|
def full_log_commits(opts = {})
|
|
356
|
-
|
|
357
|
-
|
|
289
|
+
assert_valid_opts(opts, FULL_LOG_ALLOWED_OPTS)
|
|
290
|
+
validate_log_count_option!(opts)
|
|
358
291
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
args += log_path_options(opts)
|
|
362
|
-
|
|
363
|
-
log_or_empty_on_unborn do
|
|
364
|
-
full_log = command_lines('log', *args)
|
|
365
|
-
process_commit_log_data(full_log)
|
|
366
|
-
end
|
|
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)
|
|
367
294
|
end
|
|
368
295
|
|
|
369
296
|
# Verify and resolve a Git revision to its full SHA
|
|
@@ -382,12 +309,9 @@ module Git
|
|
|
382
309
|
# @return [String] the full commit hash
|
|
383
310
|
#
|
|
384
311
|
# @raise [Git::FailedError] if the revision cannot be resolved
|
|
385
|
-
# @raise [ArgumentError] if the revision is a string starting with a hyphen
|
|
386
312
|
#
|
|
387
313
|
def rev_parse(revision)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
command('rev-parse', '--revs-only', '--end-of-options', revision, '--')
|
|
314
|
+
Git::Commands::RevParse.new(self).call(revision, '--', revs_only: true).stdout
|
|
391
315
|
end
|
|
392
316
|
|
|
393
317
|
# For backwards compatibility with the old method name
|
|
@@ -404,42 +328,69 @@ module Git
|
|
|
404
328
|
def name_rev(commit_ish)
|
|
405
329
|
assert_args_are_not_options('commit_ish', commit_ish)
|
|
406
330
|
|
|
407
|
-
|
|
331
|
+
Git::Commands::NameRev.new(self).call(commit_ish).stdout.split[1]
|
|
408
332
|
end
|
|
409
333
|
|
|
410
334
|
alias namerev name_rev
|
|
411
335
|
|
|
412
|
-
#
|
|
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.
|
|
413
341
|
#
|
|
414
342
|
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
415
343
|
#
|
|
416
|
-
# @
|
|
417
|
-
#
|
|
344
|
+
# @overload cat_file_contents(object)
|
|
345
|
+
# Returns the object's raw content as a string.
|
|
418
346
|
#
|
|
419
|
-
#
|
|
420
|
-
# 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.)
|
|
421
348
|
#
|
|
422
|
-
#
|
|
349
|
+
# @return [String] the raw content of the object
|
|
423
350
|
#
|
|
424
|
-
#
|
|
351
|
+
# @raise [ArgumentError] if `object` starts with a hyphen
|
|
425
352
|
#
|
|
426
|
-
#
|
|
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) }
|
|
427
381
|
#
|
|
428
382
|
def cat_file_contents(object)
|
|
429
383
|
assert_args_are_not_options('object', object)
|
|
430
384
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
else
|
|
441
|
-
# If a block is not given, return the file contents as a string
|
|
442
|
-
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
|
|
443
394
|
end
|
|
444
395
|
end
|
|
445
396
|
|
|
@@ -458,7 +409,7 @@ module Git
|
|
|
458
409
|
def cat_file_type(object)
|
|
459
410
|
assert_args_are_not_options('object', object)
|
|
460
411
|
|
|
461
|
-
|
|
412
|
+
cat_file_object_meta(object)[:type]
|
|
462
413
|
end
|
|
463
414
|
|
|
464
415
|
alias object_type cat_file_type
|
|
@@ -467,16 +418,16 @@ module Git
|
|
|
467
418
|
#
|
|
468
419
|
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
469
420
|
#
|
|
470
|
-
# @param object [String] the object to get the
|
|
421
|
+
# @param object [String] the object to get the size of
|
|
471
422
|
#
|
|
472
|
-
# @return [
|
|
423
|
+
# @return [Integer] the object size in bytes
|
|
473
424
|
#
|
|
474
425
|
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
475
426
|
#
|
|
476
427
|
def cat_file_size(object)
|
|
477
428
|
assert_args_are_not_options('object', object)
|
|
478
429
|
|
|
479
|
-
|
|
430
|
+
cat_file_object_meta(object)[:size]
|
|
480
431
|
end
|
|
481
432
|
|
|
482
433
|
alias object_size cat_file_size
|
|
@@ -502,7 +453,7 @@ module Git
|
|
|
502
453
|
def cat_file_commit(object)
|
|
503
454
|
assert_args_are_not_options('object', object)
|
|
504
455
|
|
|
505
|
-
cdata =
|
|
456
|
+
cdata = Git::Commands::CatFile::Raw.new(self).call('commit', object).stdout.split("\n")
|
|
506
457
|
process_commit_data(cdata, object)
|
|
507
458
|
end
|
|
508
459
|
|
|
@@ -519,6 +470,26 @@ module Git
|
|
|
519
470
|
|
|
520
471
|
CAT_FILE_HEADER_LINE = /\A(?<key>\w+) (?<value>.*)\z/
|
|
521
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
|
+
#
|
|
522
493
|
def each_cat_file_header(data)
|
|
523
494
|
while (match = CAT_FILE_HEADER_LINE.match(data.shift))
|
|
524
495
|
key = match[:key]
|
|
@@ -571,12 +542,39 @@ module Git
|
|
|
571
542
|
def cat_file_tag(object)
|
|
572
543
|
assert_args_are_not_options('object', object)
|
|
573
544
|
|
|
574
|
-
tdata =
|
|
545
|
+
tdata = Git::Commands::CatFile::Raw.new(self).call('tag', object).stdout.split("\n")
|
|
575
546
|
process_tag_data(tdata, object)
|
|
576
547
|
end
|
|
577
548
|
|
|
578
549
|
alias tag_data cat_file_tag
|
|
579
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
|
+
|
|
580
578
|
def process_tag_data(data, name)
|
|
581
579
|
hsh = { 'name' => name }
|
|
582
580
|
|
|
@@ -651,32 +649,48 @@ module Git
|
|
|
651
649
|
end
|
|
652
650
|
private_constant :RawLogParser
|
|
653
651
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
].freeze
|
|
652
|
+
# Allowed option keys for {#ls_tree}
|
|
653
|
+
LS_TREE_ALLOWED_OPTS = %i[recursive path].freeze
|
|
657
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
|
+
#
|
|
658
665
|
def ls_tree(sha, opts = {})
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
|
664
674
|
|
|
665
|
-
|
|
675
|
+
def parse_ls_tree_output(output)
|
|
676
|
+
data = { 'blob' => {}, 'tree' => {}, 'commit' => {} }
|
|
677
|
+
output.split("\n").each do |line|
|
|
666
678
|
(info, filenm) = split_status_line(line)
|
|
667
|
-
(mode, type,
|
|
668
|
-
data[type][filenm] = { mode: mode, sha:
|
|
679
|
+
(mode, type, entry_sha) = info.split
|
|
680
|
+
data[type][filenm] = { mode: mode, sha: entry_sha }
|
|
669
681
|
end
|
|
670
|
-
|
|
671
682
|
data
|
|
672
683
|
end
|
|
684
|
+
private :parse_ls_tree_output
|
|
673
685
|
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
676
690
|
end
|
|
677
691
|
|
|
678
692
|
def full_tree(sha)
|
|
679
|
-
|
|
693
|
+
Git::Commands::LsTree.new(self).call(sha, r: true).stdout.split("\n")
|
|
680
694
|
end
|
|
681
695
|
|
|
682
696
|
def tree_depth(sha)
|
|
@@ -684,38 +698,12 @@ module Git
|
|
|
684
698
|
end
|
|
685
699
|
|
|
686
700
|
def change_head_branch(branch_name)
|
|
687
|
-
|
|
701
|
+
Git::Commands::SymbolicRef::Update.new(self).call('HEAD', "refs/heads/#{branch_name}")
|
|
688
702
|
end
|
|
689
703
|
|
|
690
|
-
BRANCH_LINE_REGEXP = /
|
|
691
|
-
^
|
|
692
|
-
# Prefix indicates if this branch is checked out. The prefix is one of:
|
|
693
|
-
(?:
|
|
694
|
-
(?<current>\*[[:blank:]]) | # Current branch (checked out in the current worktree)
|
|
695
|
-
(?<worktree>\+[[:blank:]]) | # Branch checked out in a different worktree
|
|
696
|
-
[[:blank:]]{2} # Branch not checked out
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
# The branch's full refname
|
|
700
|
-
(?:
|
|
701
|
-
(?<not_a_branch>\(not[[:blank:]]a[[:blank:]]branch\)) |
|
|
702
|
-
(?:\(HEAD[[:blank:]]detached[[:blank:]]at[[:blank:]](?<detached_ref>[^)]+)\)) |
|
|
703
|
-
(?<refname>[^[[:blank:]]]+)
|
|
704
|
-
)
|
|
705
|
-
|
|
706
|
-
# Optional symref
|
|
707
|
-
# If this ref is a symbolic reference, this is the ref referenced
|
|
708
|
-
(?:
|
|
709
|
-
[[:blank:]]->[[:blank:]](?<symref>.*)
|
|
710
|
-
)?
|
|
711
|
-
$
|
|
712
|
-
/x
|
|
713
|
-
|
|
714
704
|
def branches_all
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
parse_branch_line(line, index, lines)
|
|
718
|
-
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)
|
|
719
707
|
end
|
|
720
708
|
|
|
721
709
|
def worktrees_all
|
|
@@ -730,7 +718,7 @@ module Git
|
|
|
730
718
|
# HEAD b8c63206f8d10f57892060375a86ae911fad356e
|
|
731
719
|
# detached
|
|
732
720
|
#
|
|
733
|
-
|
|
721
|
+
Git::Commands::Worktree::List.new(self).call(porcelain: true).stdout.split("\n").each do |w|
|
|
734
722
|
s = w.split
|
|
735
723
|
directory = s[1] if s[0] == 'worktree'
|
|
736
724
|
arr << [directory, s[1]] if s[0] == 'HEAD'
|
|
@@ -739,17 +727,19 @@ module Git
|
|
|
739
727
|
end
|
|
740
728
|
|
|
741
729
|
def worktree_add(dir, commitish = nil)
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
|
745
735
|
end
|
|
746
736
|
|
|
747
737
|
def worktree_remove(dir)
|
|
748
|
-
|
|
738
|
+
Git::Commands::Worktree::Remove.new(self).call(dir).stdout
|
|
749
739
|
end
|
|
750
740
|
|
|
751
741
|
def worktree_prune
|
|
752
|
-
|
|
742
|
+
Git::Commands::Worktree::Prune.new(self).call.stdout
|
|
753
743
|
end
|
|
754
744
|
|
|
755
745
|
def list_files(ref_dir)
|
|
@@ -786,7 +776,7 @@ module Git
|
|
|
786
776
|
# @return [HeadState] the state and name of the current branch
|
|
787
777
|
#
|
|
788
778
|
def current_branch_state
|
|
789
|
-
branch_name =
|
|
779
|
+
branch_name = Git::Commands::Branch::ShowCurrent.new(self).call.stdout
|
|
790
780
|
return HeadState.new(:detached, 'HEAD') if branch_name.empty?
|
|
791
781
|
|
|
792
782
|
state = get_branch_state(branch_name)
|
|
@@ -794,39 +784,36 @@ module Git
|
|
|
794
784
|
end
|
|
795
785
|
|
|
796
786
|
def branch_current
|
|
797
|
-
|
|
798
|
-
|
|
787
|
+
result = Git::Commands::Branch::ShowCurrent.new(self).call
|
|
788
|
+
name = result.stdout.strip
|
|
789
|
+
name.empty? ? 'HEAD' : name
|
|
799
790
|
end
|
|
800
791
|
|
|
801
792
|
def branch_contains(commit, branch_name = '')
|
|
802
|
-
|
|
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
|
|
803
796
|
end
|
|
804
797
|
|
|
805
|
-
|
|
806
|
-
{ keys: [:ignore_case], flag: '-i', type: :boolean },
|
|
807
|
-
{ keys: [:invert_match], flag: '-v', type: :boolean },
|
|
808
|
-
{ keys: [:extended_regexp], flag: '-E', type: :boolean },
|
|
809
|
-
# For validation only, as these are handled manually
|
|
810
|
-
{ keys: [:object], type: :validate_only },
|
|
811
|
-
{ keys: [:path_limiter], type: :validate_only }
|
|
812
|
-
].freeze
|
|
798
|
+
GREP_ALLOWED_OPTS = %i[ignore_case i invert_match v extended_regexp E object path_limiter].freeze
|
|
813
799
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
# [tree-ish] = [[line_no, match], [line_no, match2]]
|
|
817
|
-
def grep(string, opts = {})
|
|
818
|
-
opts[:object] ||= 'HEAD'
|
|
819
|
-
ArgsBuilder.validate!(opts, GREP_OPTION_MAP)
|
|
800
|
+
def grep(pattern, opts = {})
|
|
801
|
+
assert_valid_opts(opts, GREP_ALLOWED_OPTS)
|
|
820
802
|
|
|
821
|
-
|
|
822
|
-
|
|
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
|
|
823
809
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
|
827
815
|
|
|
828
|
-
|
|
829
|
-
parse_grep_output(lines)
|
|
816
|
+
parse_grep_output(result.stdout)
|
|
830
817
|
end
|
|
831
818
|
|
|
832
819
|
# Validate that the given arguments cannot be mistaken for a command-line option
|
|
@@ -879,7 +866,24 @@ module Git
|
|
|
879
866
|
raise ArgumentError, "Invalid #{arg_name}: must be a String, Pathname, or Array of Strings/Pathnames"
|
|
880
867
|
end
|
|
881
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
|
+
|
|
882
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
|
+
#
|
|
883
887
|
def handle_deprecated_path_option(opts)
|
|
884
888
|
if opts.key?(:path_limiter)
|
|
885
889
|
opts[:path_limiter]
|
|
@@ -891,74 +895,131 @@ module Git
|
|
|
891
895
|
end
|
|
892
896
|
end
|
|
893
897
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
|
898
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
|
+
#
|
|
899
930
|
def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
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)
|
|
911
940
|
end
|
|
912
941
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
+
#
|
|
918
962
|
def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
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)
|
|
930
972
|
parse_diff_stats_output(output_lines)
|
|
931
973
|
end
|
|
932
974
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
+
#
|
|
939
999
|
def diff_path_status(reference1 = nil, reference2 = nil, opts = {})
|
|
940
|
-
|
|
941
|
-
ArgsBuilder.validate!(opts, DIFF_PATH_STATUS_OPTION_MAP)
|
|
942
|
-
|
|
943
|
-
args = build_args(opts, DIFF_PATH_STATUS_OPTION_MAP)
|
|
944
|
-
args.push(reference1, reference2).compact!
|
|
1000
|
+
assert_valid_opts(opts, DIFF_PATH_STATUS_ALLOWED_OPTS)
|
|
945
1001
|
|
|
946
1002
|
path_limiter = handle_deprecated_path_option(opts)
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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)
|
|
952
1011
|
end
|
|
953
1012
|
|
|
954
1013
|
# compares the index and the working directory
|
|
955
1014
|
def diff_files
|
|
956
|
-
|
|
1015
|
+
Git::Commands::Status.new(self).call
|
|
1016
|
+
parse_raw_diff_output(Git::Commands::DiffFiles.new(self).call.stdout)
|
|
957
1017
|
end
|
|
958
1018
|
|
|
959
1019
|
# compares the index and the repository
|
|
960
1020
|
def diff_index(treeish)
|
|
961
|
-
|
|
1021
|
+
Git::Commands::Status.new(self).call
|
|
1022
|
+
parse_raw_diff_output(Git::Commands::DiffIndex.new(self).call(treeish).stdout)
|
|
962
1023
|
end
|
|
963
1024
|
|
|
964
1025
|
# List all files that are in the index
|
|
@@ -976,7 +1037,7 @@ module Git
|
|
|
976
1037
|
def ls_files(location = nil)
|
|
977
1038
|
location ||= '.'
|
|
978
1039
|
{}.tap do |files|
|
|
979
|
-
|
|
1040
|
+
Git::Commands::LsFiles.new(self).call(location, stage: true).stdout.split("\n").each do |line|
|
|
980
1041
|
(info, file) = split_status_line(line)
|
|
981
1042
|
(mode, sha, stage) = info.split
|
|
982
1043
|
files[file] = {
|
|
@@ -1009,28 +1070,22 @@ module Git
|
|
|
1009
1070
|
end
|
|
1010
1071
|
end
|
|
1011
1072
|
|
|
1012
|
-
LS_REMOTE_OPTION_MAP = [
|
|
1013
|
-
{ keys: [:refs], flag: '--refs', type: :boolean }
|
|
1014
|
-
].freeze
|
|
1015
|
-
|
|
1016
1073
|
def ls_remote(location = nil, opts = {})
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
flags = build_args(opts, LS_REMOTE_OPTION_MAP)
|
|
1020
|
-
positional_arg = location || '.'
|
|
1021
|
-
|
|
1022
|
-
output_lines = command_lines('ls-remote', *flags, positional_arg)
|
|
1074
|
+
repository = location || '.'
|
|
1075
|
+
output_lines = Git::Commands::LsRemote.new(self).call(repository, **opts).stdout.split("\n")
|
|
1023
1076
|
parse_ls_remote_output(output_lines)
|
|
1024
1077
|
end
|
|
1025
1078
|
|
|
1026
1079
|
def ignored_files
|
|
1027
|
-
|
|
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) }
|
|
1028
1083
|
end
|
|
1029
1084
|
|
|
1030
1085
|
def untracked_files
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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) }
|
|
1034
1089
|
end
|
|
1035
1090
|
|
|
1036
1091
|
def config_remote(name)
|
|
@@ -1042,19 +1097,25 @@ module Git
|
|
|
1042
1097
|
end
|
|
1043
1098
|
|
|
1044
1099
|
def config_get(name)
|
|
1045
|
-
|
|
1100
|
+
result = Git::Commands::ConfigOptionSyntax::Get.new(self).call(name)
|
|
1101
|
+
raise Git::FailedError, result if result.status.exitstatus != 0
|
|
1102
|
+
|
|
1103
|
+
result.stdout
|
|
1046
1104
|
end
|
|
1047
1105
|
|
|
1048
1106
|
def global_config_get(name)
|
|
1049
|
-
|
|
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
|
|
1050
1111
|
end
|
|
1051
1112
|
|
|
1052
1113
|
def config_list
|
|
1053
|
-
parse_config_list
|
|
1114
|
+
parse_config_list Git::Commands::ConfigOptionSyntax::List.new(self).call.stdout.split("\n")
|
|
1054
1115
|
end
|
|
1055
1116
|
|
|
1056
1117
|
def global_config_list
|
|
1057
|
-
parse_config_list
|
|
1118
|
+
parse_config_list Git::Commands::ConfigOptionSyntax::List.new(self).call(global: true).stdout.split("\n")
|
|
1058
1119
|
end
|
|
1059
1120
|
|
|
1060
1121
|
def parse_config_list(lines)
|
|
@@ -1067,7 +1128,7 @@ module Git
|
|
|
1067
1128
|
end
|
|
1068
1129
|
|
|
1069
1130
|
def parse_config(file)
|
|
1070
|
-
parse_config_list
|
|
1131
|
+
parse_config_list Git::Commands::ConfigOptionSyntax::List.new(self).call(file: file).stdout.split("\n")
|
|
1071
1132
|
end
|
|
1072
1133
|
|
|
1073
1134
|
# Shows objects
|
|
@@ -1076,34 +1137,23 @@ module Git
|
|
|
1076
1137
|
# @param [String|NilClass] path the path of the file to be shown
|
|
1077
1138
|
# @return [String] the object information
|
|
1078
1139
|
def show(objectish = nil, path = nil)
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
arr_opts << (path ? "#{objectish}:#{path}" : objectish)
|
|
1082
|
-
|
|
1083
|
-
command('show', *arr_opts.compact, chomp: false)
|
|
1140
|
+
object = path ? "#{objectish}:#{path}" : objectish
|
|
1141
|
+
Git::Commands::Show.new(self).call(*[object].compact).stdout
|
|
1084
1142
|
end
|
|
1085
1143
|
|
|
1086
1144
|
## WRITE COMMANDS ##
|
|
1087
1145
|
|
|
1088
|
-
|
|
1089
|
-
{ keys: [:file], flag: '--file', type: :valued_space }
|
|
1090
|
-
].freeze
|
|
1146
|
+
CONFIG_SET_ALLOWED_OPTS = %i[file].freeze
|
|
1091
1147
|
|
|
1092
1148
|
def config_set(name, value, options = {})
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
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))
|
|
1096
1151
|
end
|
|
1097
1152
|
|
|
1098
1153
|
def global_config_set(name, value)
|
|
1099
|
-
|
|
1154
|
+
Git::Commands::ConfigOptionSyntax::Set.new(self).call(name, value, global: true)
|
|
1100
1155
|
end
|
|
1101
1156
|
|
|
1102
|
-
ADD_OPTION_MAP = [
|
|
1103
|
-
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1104
|
-
{ keys: [:force], flag: '--force', type: :boolean }
|
|
1105
|
-
].freeze
|
|
1106
|
-
|
|
1107
1157
|
# Update the index from the current worktree to prepare the for the next commit
|
|
1108
1158
|
#
|
|
1109
1159
|
# @example
|
|
@@ -1114,31 +1164,28 @@ module Git
|
|
|
1114
1164
|
# @param [String, Array<String>] paths files to be added to the repository (relative to the worktree root)
|
|
1115
1165
|
# @param [Hash] options
|
|
1116
1166
|
#
|
|
1117
|
-
# @option options [Boolean] :all
|
|
1118
|
-
# @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)
|
|
1119
1171
|
#
|
|
1120
1172
|
def add(paths = '.', options = {})
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
args << '--'
|
|
1124
|
-
args.concat(Array(paths))
|
|
1125
|
-
|
|
1126
|
-
command('add', *args)
|
|
1173
|
+
Git::Commands::Add.new(self).call(*Array(paths), **options).stdout
|
|
1127
1174
|
end
|
|
1128
1175
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
+
#
|
|
1135
1187
|
def rm(path = '.', opts = {})
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
args << '--'
|
|
1139
|
-
args.concat(Array(path))
|
|
1140
|
-
|
|
1141
|
-
command('rm', *args)
|
|
1188
|
+
Git::Commands::Rm.new(self).call(*Array(path), **opts).stdout
|
|
1142
1189
|
end
|
|
1143
1190
|
|
|
1144
1191
|
# Returns true if the repository is empty (meaning it has no commits)
|
|
@@ -1146,7 +1193,7 @@ module Git
|
|
|
1146
1193
|
# @return [Boolean]
|
|
1147
1194
|
#
|
|
1148
1195
|
def empty?
|
|
1149
|
-
|
|
1196
|
+
Git::Commands::RevParse.new(self).call('HEAD', verify: true)
|
|
1150
1197
|
false
|
|
1151
1198
|
rescue Git::FailedError => e
|
|
1152
1199
|
raise unless e.result.status.exitstatus == 128 &&
|
|
@@ -1155,27 +1202,6 @@ module Git
|
|
|
1155
1202
|
true
|
|
1156
1203
|
end
|
|
1157
1204
|
|
|
1158
|
-
COMMIT_OPTION_MAP = [
|
|
1159
|
-
{ keys: %i[add_all all], flag: '--all', type: :boolean },
|
|
1160
|
-
{ keys: [:allow_empty], flag: '--allow-empty', type: :boolean },
|
|
1161
|
-
{ keys: [:no_verify], flag: '--no-verify', type: :boolean },
|
|
1162
|
-
{ keys: [:allow_empty_message], flag: '--allow-empty-message', type: :boolean },
|
|
1163
|
-
{ keys: [:author], flag: '--author', type: :valued_equals },
|
|
1164
|
-
{ keys: [:message], flag: '--message', type: :valued_equals },
|
|
1165
|
-
{ keys: [:no_gpg_sign], flag: '--no-gpg-sign', type: :boolean },
|
|
1166
|
-
{ keys: [:date], flag: '--date', type: :valued_equals, validator: ->(v) { v.is_a?(String) } },
|
|
1167
|
-
{ keys: [:amend], type: :custom, builder: ->(value) { ['--amend', '--no-edit'] if value } },
|
|
1168
|
-
{
|
|
1169
|
-
keys: [:gpg_sign],
|
|
1170
|
-
type: :custom,
|
|
1171
|
-
builder: lambda { |value|
|
|
1172
|
-
if value
|
|
1173
|
-
value == true ? '--gpg-sign' : "--gpg-sign=#{value}"
|
|
1174
|
-
end
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
].freeze
|
|
1178
|
-
|
|
1179
1205
|
# Takes the commit message with the options and executes the commit command
|
|
1180
1206
|
#
|
|
1181
1207
|
# accepts options:
|
|
@@ -1191,176 +1217,284 @@ module Git
|
|
|
1191
1217
|
#
|
|
1192
1218
|
# @param [String] message the commit message to be used
|
|
1193
1219
|
# @param [Hash] opts the commit options to be used
|
|
1194
|
-
|
|
1220
|
+
#
|
|
1195
1221
|
def commit(message, opts = {})
|
|
1196
|
-
opts
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
raise ArgumentError, 'cannot specify :gpg_sign and :no_gpg_sign' if opts[:gpg_sign] && opts[:no_gpg_sign]
|
|
1200
|
-
|
|
1201
|
-
ArgsBuilder.validate!(opts, COMMIT_OPTION_MAP)
|
|
1202
|
-
|
|
1203
|
-
args = build_args(opts, COMMIT_OPTION_MAP)
|
|
1204
|
-
command('commit', *args)
|
|
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
|
|
1205
1225
|
end
|
|
1206
|
-
RESET_OPTION_MAP = [
|
|
1207
|
-
{ keys: [:hard], flag: '--hard', type: :boolean }
|
|
1208
|
-
].freeze
|
|
1209
1226
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1227
|
+
# @return [String] the command output
|
|
1228
|
+
#
|
|
1229
|
+
def reset(commit = nil, opts = {})
|
|
1230
|
+
Git::Commands::Reset.new(self).call(commit, **opts).stdout
|
|
1214
1231
|
end
|
|
1215
1232
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
{ keys: [:ff], flag: '-ff', type: :boolean },
|
|
1219
|
-
{ keys: [:d], flag: '-d', type: :boolean },
|
|
1220
|
-
{ keys: [:x], flag: '-x', type: :boolean }
|
|
1221
|
-
].freeze
|
|
1222
|
-
|
|
1233
|
+
# @return [String] the command output
|
|
1234
|
+
#
|
|
1223
1235
|
def clean(opts = {})
|
|
1224
|
-
|
|
1225
|
-
|
|
1236
|
+
opts = migrate_clean_legacy_options(opts)
|
|
1237
|
+
Git::Commands::Clean.new(self).call(**opts).stdout
|
|
1226
1238
|
end
|
|
1227
1239
|
|
|
1228
|
-
|
|
1229
|
-
{ keys: [:no_edit], flag: '--no-edit', type: :boolean }
|
|
1230
|
-
].freeze
|
|
1240
|
+
REVERT_ALLOWED_OPTS = %i[no_edit].freeze
|
|
1231
1241
|
|
|
1232
1242
|
def revert(commitish, opts = {})
|
|
1233
|
-
|
|
1243
|
+
assert_valid_opts(opts, REVERT_ALLOWED_OPTS)
|
|
1234
1244
|
opts = { no_edit: true }.merge(opts)
|
|
1235
|
-
|
|
1236
|
-
args = build_args(opts, REVERT_OPTION_MAP)
|
|
1237
|
-
args << commitish
|
|
1238
|
-
|
|
1239
|
-
command('revert', *args)
|
|
1245
|
+
Git::Commands::Revert::Start.new(self).call(commitish, **opts).stdout
|
|
1240
1246
|
end
|
|
1241
1247
|
|
|
1242
1248
|
def apply(patch_file)
|
|
1243
|
-
|
|
1244
|
-
arr_opts << '--' << patch_file if patch_file
|
|
1245
|
-
command('apply', *arr_opts)
|
|
1249
|
+
Git::Commands::Apply.new(self).call(*[patch_file].compact, chdir: @git_work_dir).stdout
|
|
1246
1250
|
end
|
|
1247
1251
|
|
|
1248
1252
|
def apply_mail(patch_file)
|
|
1249
|
-
|
|
1250
|
-
arr_opts << '--' << patch_file if patch_file
|
|
1251
|
-
command('am', *arr_opts)
|
|
1253
|
+
Git::Commands::Am::Apply.new(self).call(*[patch_file].compact, chdir: @git_work_dir).stdout
|
|
1252
1254
|
end
|
|
1253
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
|
+
#
|
|
1254
1271
|
def stashes_all
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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) }
|
|
1258
1275
|
end
|
|
1259
1276
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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')
|
|
1263
1294
|
end
|
|
1264
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
|
+
#
|
|
1265
1312
|
def stash_apply(id = nil)
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
else
|
|
1269
|
-
command('stash', 'apply')
|
|
1270
|
-
end
|
|
1313
|
+
result = Git::Commands::Stash::Apply.new(self).call(id)
|
|
1314
|
+
result.stdout
|
|
1271
1315
|
end
|
|
1272
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
|
+
#
|
|
1273
1328
|
def stash_clear
|
|
1274
|
-
|
|
1329
|
+
result = Git::Commands::Stash::Clear.new(self).call
|
|
1330
|
+
result.stdout
|
|
1275
1331
|
end
|
|
1276
1332
|
|
|
1277
|
-
|
|
1278
|
-
|
|
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")
|
|
1279
1350
|
end
|
|
1280
1351
|
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
|
1283
1363
|
end
|
|
1284
1364
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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)
|
|
1288
1379
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
].freeze
|
|
1380
|
+
raise Git::Error, result.stderr.strip unless result.status.success?
|
|
1381
|
+
|
|
1382
|
+
result.stdout.strip
|
|
1383
|
+
end
|
|
1294
1384
|
|
|
1295
1385
|
# Runs checkout command to checkout or create branch
|
|
1296
1386
|
#
|
|
1297
1387
|
# accepts options:
|
|
1298
|
-
# :new_branch
|
|
1299
|
-
# :force
|
|
1300
|
-
# :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
|
|
1301
1395
|
#
|
|
1302
|
-
# @param [String] branch
|
|
1303
|
-
# @param [Hash] opts
|
|
1304
1396
|
def checkout(branch = nil, opts = {})
|
|
1305
1397
|
if branch.is_a?(Hash) && opts.empty?
|
|
1306
1398
|
opts = branch
|
|
1307
1399
|
branch = nil
|
|
1308
1400
|
end
|
|
1309
|
-
ArgsBuilder.validate!(opts, CHECKOUT_OPTION_MAP)
|
|
1310
1401
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1402
|
+
target, translated_opts = translate_checkout_opts(branch, opts)
|
|
1403
|
+
Git::Commands::Checkout::Branch.new(self).call(target, **translated_opts).stdout
|
|
1404
|
+
end
|
|
1313
1405
|
|
|
1314
|
-
|
|
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
|
|
1315
1417
|
end
|
|
1418
|
+
private :translate_checkout_opts
|
|
1316
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
|
+
#
|
|
1317
1426
|
def checkout_file(version, file)
|
|
1318
|
-
|
|
1319
|
-
arr_opts << version
|
|
1320
|
-
arr_opts << file
|
|
1321
|
-
command('checkout', *arr_opts)
|
|
1427
|
+
Git::Commands::Checkout::Files.new(self).call(version, pathspec: [file]).stdout
|
|
1322
1428
|
end
|
|
1323
1429
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
]
|
|
1329
|
-
|
|
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
|
+
#
|
|
1330
1452
|
def merge(branch, message = nil, opts = {})
|
|
1331
|
-
#
|
|
1332
|
-
opts
|
|
1333
|
-
ArgsBuilder.validate!(opts, MERGE_OPTION_MAP)
|
|
1453
|
+
# Handle legacy positional message argument
|
|
1454
|
+
opts = opts.merge(message: message) if message
|
|
1334
1455
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1456
|
+
# Map legacy option names to new interface
|
|
1457
|
+
opts = translate_merge_options(opts)
|
|
1337
1458
|
|
|
1338
|
-
|
|
1459
|
+
Git::Commands::Merge::Start.new(self).call(*Array(branch), no_edit: true, **opts).stdout
|
|
1339
1460
|
end
|
|
1340
1461
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
+
#
|
|
1348
1477
|
def merge_base(*args)
|
|
1349
1478
|
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
flags = build_args(opts, MERGE_BASE_OPTION_MAP)
|
|
1353
|
-
command_args = flags + args
|
|
1354
|
-
|
|
1355
|
-
command('merge-base', *command_args).lines.map(&:strip)
|
|
1479
|
+
result = Git::Commands::MergeBase.new(self).call(*args, **opts)
|
|
1480
|
+
result.stdout.lines.map(&:strip).reject(&:empty?)
|
|
1356
1481
|
end
|
|
1357
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
|
+
#
|
|
1358
1493
|
def unmerged
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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 (.*)/
|
|
1362
1497
|
end
|
|
1363
|
-
unmerged
|
|
1364
1498
|
end
|
|
1365
1499
|
|
|
1366
1500
|
def conflicts # :yields: file, your, their
|
|
@@ -1376,416 +1510,961 @@ module Git
|
|
|
1376
1510
|
end
|
|
1377
1511
|
end
|
|
1378
1512
|
|
|
1379
|
-
REMOTE_ADD_OPTION_MAP = [
|
|
1380
|
-
{ keys: %i[with_fetch fetch], flag: '-f', type: :boolean },
|
|
1381
|
-
{ keys: [:track], flag: '-t', type: :valued_space }
|
|
1382
|
-
].freeze
|
|
1383
|
-
|
|
1384
1513
|
def remote_add(name, url, opts = {})
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
flags = build_args(opts, REMOTE_ADD_OPTION_MAP)
|
|
1388
|
-
positional_args = ['--', name, url]
|
|
1389
|
-
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)
|
|
1390
1516
|
|
|
1391
|
-
|
|
1517
|
+
Git::Commands::Remote::Add.new(self).call(name, url, **translated_opts)
|
|
1392
1518
|
end
|
|
1393
1519
|
|
|
1394
|
-
REMOTE_SET_BRANCHES_OPTION_MAP = [
|
|
1395
|
-
{ keys: [:add], flag: '--add', type: :boolean }
|
|
1396
|
-
].freeze
|
|
1397
|
-
|
|
1398
1520
|
def remote_set_branches(name, branches, opts = {})
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
flags = build_args(opts, REMOTE_SET_BRANCHES_OPTION_MAP)
|
|
1402
|
-
branch_args = Array(branches).flatten
|
|
1403
|
-
command_args = ['set-branches'] + flags + [name] + branch_args
|
|
1404
|
-
|
|
1405
|
-
command('remote', *command_args)
|
|
1521
|
+
Git::Commands::Remote::SetBranches.new(self).call(name, *Array(branches).flatten, **opts)
|
|
1406
1522
|
end
|
|
1407
1523
|
|
|
1408
|
-
def remote_set_url(name, url)
|
|
1409
|
-
|
|
1410
|
-
arr_opts << name
|
|
1411
|
-
arr_opts << url
|
|
1412
|
-
|
|
1413
|
-
command('remote', *arr_opts)
|
|
1524
|
+
def remote_set_url(name, url, opts = {})
|
|
1525
|
+
Git::Commands::Remote::SetUrl.new(self).call(name, url, **opts)
|
|
1414
1526
|
end
|
|
1415
1527
|
|
|
1416
1528
|
def remote_remove(name)
|
|
1417
|
-
|
|
1529
|
+
Git::Commands::Remote::Remove.new(self).call(name)
|
|
1418
1530
|
end
|
|
1419
1531
|
|
|
1420
1532
|
def remotes
|
|
1421
|
-
|
|
1533
|
+
Git::Commands::Remote::List.new(self).call.stdout.split("\n")
|
|
1422
1534
|
end
|
|
1423
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
|
+
#
|
|
1424
1542
|
def tags
|
|
1425
|
-
|
|
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)
|
|
1426
1545
|
end
|
|
1427
1546
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
+
#
|
|
1436
1627
|
def tag(name, *args)
|
|
1437
1628
|
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
1438
1629
|
target = args.first
|
|
1439
1630
|
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
|
1442
1645
|
|
|
1443
|
-
|
|
1444
|
-
positional_args = [name, target].compact
|
|
1646
|
+
PUSH_ALLOWED_OPTS = %i[mirror delete force f push_option all tags].freeze
|
|
1445
1647
|
|
|
1446
|
-
|
|
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)
|
|
1739
|
+
|
|
1740
|
+
first_result = push_refs(remote, branch, opts)
|
|
1741
|
+
return first_result.stdout unless push_tags_separately?(opts)
|
|
1742
|
+
|
|
1743
|
+
push_tags(remote, opts).stdout
|
|
1447
1744
|
end
|
|
1448
1745
|
|
|
1449
|
-
|
|
1450
|
-
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1451
|
-
{ keys: %i[tags t], flag: '--tags', type: :boolean },
|
|
1452
|
-
{ keys: %i[prune p], flag: '--prune', type: :boolean },
|
|
1453
|
-
{ keys: %i[prune-tags P], flag: '--prune-tags', type: :boolean },
|
|
1454
|
-
{ keys: %i[force f], flag: '--force', type: :boolean },
|
|
1455
|
-
{ keys: %i[update-head-ok u], flag: '--update-head-ok', type: :boolean },
|
|
1456
|
-
{ keys: [:unshallow], flag: '--unshallow', type: :boolean },
|
|
1457
|
-
{ keys: [:depth], flag: '--depth', type: :valued_space },
|
|
1458
|
-
{ keys: [:ref], type: :validate_only }
|
|
1459
|
-
].freeze
|
|
1746
|
+
PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
|
|
1460
1747
|
|
|
1461
|
-
def
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
|
1756
|
+
|
|
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
|
|
1464
1781
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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)
|
|
1469
1868
|
end
|
|
1869
|
+
apply_gzip(file) if gzip
|
|
1470
1870
|
|
|
1471
|
-
|
|
1871
|
+
file
|
|
1472
1872
|
end
|
|
1473
1873
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
|
1482
1895
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
|
1486
1907
|
|
|
1487
|
-
|
|
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
|
|
1488
1917
|
|
|
1489
|
-
|
|
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
|
|
1490
1931
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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."
|
|
1496
2001
|
end
|
|
2002
|
+
true
|
|
1497
2003
|
end
|
|
1498
2004
|
|
|
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
|
|
2017
|
+
|
|
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
|
|
1501
2030
|
].freeze
|
|
1502
2031
|
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
|
2117
|
+
|
|
2118
|
+
extra_options = options_hash.keys - COMMAND_CAPTURING_ARG_DEFAULTS.keys
|
|
2119
|
+
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
1505
2120
|
|
|
1506
|
-
|
|
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)
|
|
2124
|
+
end
|
|
2125
|
+
|
|
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
|
|
1507
2191
|
|
|
1508
|
-
|
|
1509
|
-
|
|
2192
|
+
extra_options = options_hash.keys - COMMAND_STREAMING_ARG_DEFAULTS.keys
|
|
2193
|
+
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
1510
2194
|
|
|
1511
|
-
|
|
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)
|
|
1512
2198
|
end
|
|
1513
2199
|
|
|
1514
|
-
|
|
1515
|
-
head = File.join(@git_dir, 'refs', 'tags', tag_name)
|
|
1516
|
-
return File.read(head).chomp if File.exist?(head)
|
|
1517
|
-
|
|
1518
|
-
begin
|
|
1519
|
-
command('show-ref', '--tags', '-s', tag_name)
|
|
1520
|
-
rescue Git::FailedError => e
|
|
1521
|
-
raise unless e.result.status.exitstatus == 1 && e.result.stderr == ''
|
|
1522
|
-
|
|
1523
|
-
''
|
|
1524
|
-
end
|
|
1525
|
-
end
|
|
2200
|
+
private
|
|
1526
2201
|
|
|
1527
|
-
def
|
|
1528
|
-
|
|
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.')
|
|
1529
2205
|
end
|
|
1530
2206
|
|
|
1531
|
-
def
|
|
1532
|
-
|
|
1533
|
-
end
|
|
1534
|
-
|
|
1535
|
-
FSCK_OPTION_MAP = [
|
|
1536
|
-
{ flag: '--no-progress', type: :static },
|
|
1537
|
-
{ keys: [:unreachable], flag: '--unreachable', type: :boolean },
|
|
1538
|
-
{ keys: [:strict], flag: '--strict', type: :boolean },
|
|
1539
|
-
{ keys: [:connectivity_only], flag: '--connectivity-only', type: :boolean },
|
|
1540
|
-
{ keys: [:root], flag: '--root', type: :boolean },
|
|
1541
|
-
{ keys: [:tags], flag: '--tags', type: :boolean },
|
|
1542
|
-
{ keys: [:cache], flag: '--cache', type: :boolean },
|
|
1543
|
-
{ keys: [:no_reflogs], flag: '--no-reflogs', type: :boolean },
|
|
1544
|
-
{ keys: [:lost_found], flag: '--lost-found', type: :boolean },
|
|
1545
|
-
{ keys: [:dangling], flag: '--dangling', type: :boolean_negatable },
|
|
1546
|
-
{ keys: [:full], flag: '--full', type: :boolean_negatable },
|
|
1547
|
-
{ keys: [:name_objects], flag: '--name-objects', type: :boolean_negatable },
|
|
1548
|
-
{ keys: [:references], flag: '--references', type: :boolean_negatable }
|
|
1549
|
-
].freeze
|
|
2207
|
+
def deprecate_clean_option(opts, key, message)
|
|
2208
|
+
return opts unless opts.key?(key)
|
|
1550
2209
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
# fsck returns non-zero exit status when issues are found:
|
|
1555
|
-
# 1 = errors found, 2 = missing objects, 4 = warnings
|
|
1556
|
-
# We still want to parse the output in these cases
|
|
1557
|
-
output = begin
|
|
1558
|
-
command('fsck', *args)
|
|
1559
|
-
rescue Git::FailedError => e
|
|
1560
|
-
raise unless [1, 2, 4].include?(e.result.status.exitstatus)
|
|
1561
|
-
|
|
1562
|
-
e.result.stdout
|
|
1563
|
-
end
|
|
1564
|
-
parse_fsck_output(output)
|
|
1565
|
-
end
|
|
2210
|
+
opts = opts.dup
|
|
2211
|
+
deprecated_value = opts.delete(key)
|
|
2212
|
+
validate_deprecated_clean_option_value!(key, deprecated_value)
|
|
1566
2213
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
].freeze
|
|
2214
|
+
Git::Deprecation.warn(message)
|
|
2215
|
+
return opts unless deprecated_value
|
|
1570
2216
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
flags = build_args(opts, READ_TREE_OPTION_MAP)
|
|
1574
|
-
command('read-tree', *flags, treeish)
|
|
2217
|
+
opts[:force] = merge_clean_force_option(opts[:force], force_specified: force_option_specified?(opts))
|
|
2218
|
+
opts
|
|
1575
2219
|
end
|
|
1576
2220
|
|
|
1577
|
-
def
|
|
1578
|
-
|
|
2221
|
+
def force_option_specified?(opts)
|
|
2222
|
+
opts.key?(:force) && !opts[:force].nil?
|
|
1579
2223
|
end
|
|
1580
2224
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
{ keys: [:message], flag: '-m', type: :valued_space }
|
|
1584
|
-
].freeze
|
|
1585
|
-
|
|
1586
|
-
def commit_tree(tree, opts = {})
|
|
1587
|
-
opts[:message] ||= "commit tree #{tree}"
|
|
1588
|
-
ArgsBuilder.validate!(opts, COMMIT_TREE_OPTION_MAP)
|
|
1589
|
-
|
|
1590
|
-
flags = build_args(opts, COMMIT_TREE_OPTION_MAP)
|
|
1591
|
-
command('commit-tree', tree, *flags)
|
|
1592
|
-
end
|
|
2225
|
+
def validate_deprecated_clean_option_value!(key, value)
|
|
2226
|
+
return if value.nil? || value == true || value == false
|
|
1593
2227
|
|
|
1594
|
-
|
|
1595
|
-
command('update-ref', ref, commit)
|
|
2228
|
+
raise ArgumentError, "#{key} option only accepts true, false, or nil"
|
|
1596
2229
|
end
|
|
1597
2230
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
{ keys: [:force], flag: '--force', type: :boolean },
|
|
1601
|
-
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1602
|
-
{ keys: [:path_limiter], type: :validate_only }
|
|
1603
|
-
].freeze
|
|
2231
|
+
def merge_clean_force_option(existing_force, force_specified: false)
|
|
2232
|
+
return 2 unless force_specified
|
|
1604
2233
|
|
|
1605
|
-
|
|
1606
|
-
ArgsBuilder.validate!(opts, CHECKOUT_INDEX_OPTION_MAP)
|
|
1607
|
-
args = build_args(opts, CHECKOUT_INDEX_OPTION_MAP)
|
|
2234
|
+
normalized_force = normalize_clean_force_option(existing_force)
|
|
1608
2235
|
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
|
1611
2242
|
end
|
|
1612
|
-
|
|
1613
|
-
command('checkout-index', *args)
|
|
1614
2243
|
end
|
|
1615
2244
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
{ keys: [:remote], flag: '--remote', type: :valued_equals },
|
|
1619
|
-
# These options are used by helpers or handled manually
|
|
1620
|
-
{ keys: [:path], type: :validate_only },
|
|
1621
|
-
{ keys: [:format], type: :validate_only },
|
|
1622
|
-
{ keys: [:add_gzip], type: :validate_only }
|
|
1623
|
-
].freeze
|
|
1624
|
-
|
|
1625
|
-
def archive(sha, file = nil, opts = {})
|
|
1626
|
-
ArgsBuilder.validate!(opts, ARCHIVE_OPTION_MAP)
|
|
1627
|
-
file ||= temp_file_name
|
|
1628
|
-
format, gzip = parse_archive_format_options(opts)
|
|
1629
|
-
|
|
1630
|
-
args = build_args(opts, ARCHIVE_OPTION_MAP)
|
|
1631
|
-
args.unshift("--format=#{format}")
|
|
1632
|
-
args << sha
|
|
1633
|
-
args.push('--', opts[:path]) if opts[:path]
|
|
1634
|
-
|
|
1635
|
-
File.open(file, 'wb') { |f| command('archive', *args, out: f) }
|
|
1636
|
-
apply_gzip(file) if gzip
|
|
2245
|
+
def merge_integer_clean_force_option(normalized_force)
|
|
2246
|
+
return normalized_force if normalized_force < 1
|
|
1637
2247
|
|
|
1638
|
-
|
|
2248
|
+
[normalized_force, 2].max
|
|
1639
2249
|
end
|
|
1640
2250
|
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
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
|
|
1647
2256
|
end
|
|
1648
2257
|
|
|
1649
|
-
#
|
|
2258
|
+
# Build a result hash from clone options for Git::Base.new
|
|
1650
2259
|
#
|
|
1651
|
-
#
|
|
1652
|
-
#
|
|
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>'...
|
|
1653
2264
|
#
|
|
1654
|
-
#
|
|
1655
|
-
# lib.compare_version_to(2, 42, 0) #=> 0
|
|
1656
|
-
# lib.compare_version_to(2, 43, 0) #=> -1
|
|
2265
|
+
# @param command_line_result [Git::CommandLineResult] the result of the git clone command
|
|
1657
2266
|
#
|
|
1658
|
-
# @param
|
|
1659
|
-
# @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)
|
|
1660
2268
|
#
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
|
1663
2277
|
end
|
|
1664
2278
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
|
1668
2301
|
|
|
1669
|
-
|
|
1670
|
-
(current_command_version <=> required_command_version) >= 0
|
|
2302
|
+
[match[2], !match[1].nil?]
|
|
1671
2303
|
end
|
|
1672
2304
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
|
1677
2317
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
warn "[WARNING] The git gem requires git #{lib.required_command_version.join('.')} or later, " \
|
|
1681
|
-
"but only found #{lib.current_command_version.join('.')}. You should probably upgrade."
|
|
2318
|
+
%i[working_directory repository].each do |key|
|
|
2319
|
+
result[key] = File.join(chdir, result[key]) if result.key?(key)
|
|
1682
2320
|
end
|
|
1683
|
-
true
|
|
1684
2321
|
end
|
|
1685
2322
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
STATIC_GLOBAL_OPTS = %w[
|
|
1697
|
-
-c core.quotePath=true
|
|
1698
|
-
-c color.ui=false
|
|
1699
|
-
-c color.advice=false
|
|
1700
|
-
-c color.diff=false
|
|
1701
|
-
-c color.grep=false
|
|
1702
|
-
-c color.push=false
|
|
1703
|
-
-c color.remote=false
|
|
1704
|
-
-c color.showBranch=false
|
|
1705
|
-
-c color.status=false
|
|
1706
|
-
-c color.transport=false
|
|
1707
|
-
].freeze
|
|
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)
|
|
1708
2333
|
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
{ keys: [:since], flag: '--since', type: :valued_equals },
|
|
1714
|
-
{ keys: [:until], flag: '--until', type: :valued_equals },
|
|
1715
|
-
{ keys: [:grep], flag: '--grep', type: :valued_equals },
|
|
1716
|
-
{ keys: [:author], flag: '--author', type: :valued_equals },
|
|
1717
|
-
{ keys: [:count], flag: '--max-count', type: :valued_equals },
|
|
1718
|
-
{ keys: [:between], type: :custom, builder: ->(value) { "#{value[0]}..#{value[1]}" if value } }
|
|
1719
|
-
].freeze
|
|
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
|
|
1720
2338
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
FSCK_ROOT_PATTERN = /\Aroot ([0-9a-f]{40})\z/
|
|
1724
|
-
FSCK_TAGGED_PATTERN = /\Atagged (\w+) ([0-9a-f]{40}) \((.+)\) in ([0-9a-f]{40})\z/
|
|
2339
|
+
def deprecate_clone_recursive_option!(opts)
|
|
2340
|
+
return unless opts.key?(:recursive)
|
|
1725
2341
|
|
|
1726
|
-
|
|
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
|
|
1727
2345
|
|
|
1728
|
-
|
|
2346
|
+
def deprecate_clone_remote_option!(opts)
|
|
2347
|
+
return unless opts.key?(:remote)
|
|
1729
2348
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
output.each_line { |line| parse_fsck_line(line.strip, result) }
|
|
1733
|
-
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)
|
|
1734
2351
|
end
|
|
1735
2352
|
|
|
1736
|
-
def
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
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)
|
|
1741
2357
|
end
|
|
1742
2358
|
|
|
1743
|
-
def
|
|
1744
|
-
return unless
|
|
2359
|
+
def deprecate_commit_add_all_option!(opts)
|
|
2360
|
+
return unless opts.key?(:add_all)
|
|
1745
2361
|
|
|
1746
|
-
|
|
2362
|
+
Git::Deprecation.warn('The :add_all option for Git::Lib#commit is deprecated, use :all instead')
|
|
2363
|
+
opts[:all] = opts.delete(:add_all)
|
|
1747
2364
|
end
|
|
1748
2365
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
|
1753
2376
|
end
|
|
1754
2377
|
|
|
1755
|
-
|
|
1756
|
-
|
|
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)
|
|
1757
2388
|
|
|
1758
|
-
result
|
|
2389
|
+
result
|
|
1759
2390
|
end
|
|
1760
2391
|
|
|
1761
|
-
|
|
1762
|
-
|
|
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?(':')
|
|
1763
2406
|
|
|
1764
|
-
|
|
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
|
|
1765
2413
|
end
|
|
1766
2414
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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
|
|
1772
2428
|
end
|
|
1773
2429
|
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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/) }
|
|
1783
2442
|
end
|
|
1784
2443
|
|
|
1785
2444
|
def build_args(opts, option_map)
|
|
1786
2445
|
Git::ArgsBuilder.new(opts, option_map).build
|
|
1787
2446
|
end
|
|
1788
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
|
+
|
|
1789
2468
|
def initialize_from_base(base_object)
|
|
1790
2469
|
@git_dir = base_object.repo.to_s
|
|
1791
2470
|
@git_index_file = base_object.index&.to_s
|
|
@@ -1800,15 +2479,6 @@ module Git
|
|
|
1800
2479
|
@git_ssh = base_hash.key?(:git_ssh) ? base_hash[:git_ssh] : :use_global_config
|
|
1801
2480
|
end
|
|
1802
2481
|
|
|
1803
|
-
def return_base_opts_from_clone(clone_dir, opts)
|
|
1804
|
-
base_opts = {}
|
|
1805
|
-
base_opts[:repository] = clone_dir if opts[:bare] || opts[:mirror]
|
|
1806
|
-
base_opts[:working_directory] = clone_dir unless opts[:bare] || opts[:mirror]
|
|
1807
|
-
base_opts[:log] = opts[:log] if opts[:log]
|
|
1808
|
-
base_opts[:git_ssh] = opts[:git_ssh] if opts.key?(:git_ssh)
|
|
1809
|
-
base_opts
|
|
1810
|
-
end
|
|
1811
|
-
|
|
1812
2482
|
def process_commit_headers(data)
|
|
1813
2483
|
headers = { 'parent' => [] } # Pre-initialize for multiple parents
|
|
1814
2484
|
each_cat_file_header(data) do |key, value|
|
|
@@ -1821,44 +2491,8 @@ module Git
|
|
|
1821
2491
|
headers
|
|
1822
2492
|
end
|
|
1823
2493
|
|
|
1824
|
-
def parse_branch_line(line, index, all_lines)
|
|
1825
|
-
match_data = match_branch_line(line, index, all_lines)
|
|
1826
|
-
|
|
1827
|
-
return nil if match_data[:not_a_branch] || match_data[:detached_ref]
|
|
1828
|
-
|
|
1829
|
-
format_branch_data(match_data)
|
|
1830
|
-
end
|
|
1831
|
-
|
|
1832
|
-
def match_branch_line(line, index, all_lines)
|
|
1833
|
-
match_data = line.match(BRANCH_LINE_REGEXP)
|
|
1834
|
-
raise Git::UnexpectedResultError, unexpected_branch_line_error(all_lines, line, index) unless match_data
|
|
1835
|
-
|
|
1836
|
-
match_data
|
|
1837
|
-
end
|
|
1838
|
-
|
|
1839
|
-
def format_branch_data(match_data)
|
|
1840
|
-
[
|
|
1841
|
-
match_data[:refname],
|
|
1842
|
-
!match_data[:current].nil?,
|
|
1843
|
-
!match_data[:worktree].nil?,
|
|
1844
|
-
match_data[:symref]
|
|
1845
|
-
]
|
|
1846
|
-
end
|
|
1847
|
-
|
|
1848
|
-
def unexpected_branch_line_error(lines, line, index)
|
|
1849
|
-
<<~ERROR
|
|
1850
|
-
Unexpected line in output from `git branch -a`, line #{index + 1}
|
|
1851
|
-
|
|
1852
|
-
Full output:
|
|
1853
|
-
#{lines.join("\n ")}
|
|
1854
|
-
|
|
1855
|
-
Line #{index + 1}:
|
|
1856
|
-
"#{line}"
|
|
1857
|
-
ERROR
|
|
1858
|
-
end
|
|
1859
|
-
|
|
1860
2494
|
def get_branch_state(branch_name)
|
|
1861
|
-
|
|
2495
|
+
Git::Commands::RevParse.new(self).call(branch_name, verify: true, quiet: true)
|
|
1862
2496
|
:active
|
|
1863
2497
|
rescue Git::FailedError => e
|
|
1864
2498
|
# An exit status of 1 with empty stderr from `rev-parse --verify`
|
|
@@ -1868,23 +2502,14 @@ module Git
|
|
|
1868
2502
|
:unborn
|
|
1869
2503
|
end
|
|
1870
2504
|
|
|
1871
|
-
def
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
|
|
1876
|
-
|
|
1877
|
-
[] # 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
|
|
1878
2509
|
end
|
|
1879
2510
|
|
|
1880
|
-
def parse_grep_output(
|
|
1881
|
-
|
|
1882
|
-
match = line.match(/\A(.*?):(\d+):(.*)/)
|
|
1883
|
-
next unless match
|
|
1884
|
-
|
|
1885
|
-
_full, filename, line_num, text = match.to_a
|
|
1886
|
-
hsh[filename] << [line_num.to_i, text]
|
|
1887
|
-
end
|
|
2511
|
+
def parse_grep_output(output)
|
|
2512
|
+
Git::Parsers::Grep.parse(output)
|
|
1888
2513
|
end
|
|
1889
2514
|
|
|
1890
2515
|
def parse_diff_stats_output(lines)
|
|
@@ -1909,6 +2534,18 @@ module Git
|
|
|
1909
2534
|
parts
|
|
1910
2535
|
end
|
|
1911
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
|
+
|
|
1912
2549
|
def build_final_stats_hash(file_stats)
|
|
1913
2550
|
{
|
|
1914
2551
|
total: build_total_stats(file_stats),
|
|
@@ -1954,45 +2591,48 @@ module Git
|
|
|
1954
2591
|
[type, name, value]
|
|
1955
2592
|
end
|
|
1956
2593
|
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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
|
|
1966
2606
|
match_data = full_message.match(/^[^:]+:(.*)$/)
|
|
1967
2607
|
message = match_data ? match_data[1] : full_message
|
|
1968
2608
|
|
|
1969
2609
|
[index, message.strip]
|
|
1970
2610
|
end
|
|
1971
2611
|
|
|
1972
|
-
#
|
|
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
|
|
1973
2618
|
#
|
|
1974
2619
|
# @param path [String] the path to the file in the index
|
|
1975
2620
|
#
|
|
1976
|
-
# @param stage [Integer] the stage
|
|
2621
|
+
# @param stage [Integer] the index stage to read (e.g., `1` ancestor, `2` ours, `3` theirs)
|
|
1977
2622
|
#
|
|
1978
|
-
# @param out_io [IO] the IO object to
|
|
2623
|
+
# @param out_io [IO] the `IO` object to stream the staged content into
|
|
1979
2624
|
#
|
|
1980
|
-
# @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
|
|
1981
2630
|
#
|
|
1982
2631
|
def write_staged_content(path, stage, out_io)
|
|
1983
|
-
|
|
2632
|
+
Git::Commands::Show.new(self).call(":#{stage}:#{path}", out: out_io)
|
|
1984
2633
|
out_io
|
|
1985
2634
|
end
|
|
1986
2635
|
|
|
1987
|
-
def validate_tag_options!(opts)
|
|
1988
|
-
is_annotated = opts[:a] || opts[:annotate]
|
|
1989
|
-
has_message = opts[:m] || opts[:message]
|
|
1990
|
-
|
|
1991
|
-
return unless is_annotated && !has_message
|
|
1992
|
-
|
|
1993
|
-
raise ArgumentError, 'Cannot create an annotated tag without a message.'
|
|
1994
|
-
end
|
|
1995
|
-
|
|
1996
2636
|
def normalize_push_args(remote, branch, opts)
|
|
1997
2637
|
if branch.is_a?(Hash)
|
|
1998
2638
|
opts = branch
|
|
@@ -2008,15 +2648,22 @@ module Git
|
|
|
2008
2648
|
[remote, branch, opts]
|
|
2009
2649
|
end
|
|
2010
2650
|
|
|
2011
|
-
def
|
|
2012
|
-
|
|
2013
|
-
|
|
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
|
|
2014
2664
|
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
args << remote if remote
|
|
2018
|
-
args << branch if branch
|
|
2019
|
-
args
|
|
2665
|
+
def push_tags(remote, opts)
|
|
2666
|
+
Git::Commands::Push.new(self).call(*[remote].compact, **opts)
|
|
2020
2667
|
end
|
|
2021
2668
|
|
|
2022
2669
|
def temp_file_name
|
|
@@ -2038,16 +2685,6 @@ module Git
|
|
|
2038
2685
|
Zlib::GzipWriter.open(file) { |gz| gz.write(file_content) }
|
|
2039
2686
|
end
|
|
2040
2687
|
|
|
2041
|
-
def command_lines(cmd, *opts, chdir: nil)
|
|
2042
|
-
cmd_op = command(cmd, *opts, chdir: chdir)
|
|
2043
|
-
op = if cmd_op.encoding.name == 'UTF-8'
|
|
2044
|
-
cmd_op
|
|
2045
|
-
else
|
|
2046
|
-
cmd_op.encode('UTF-8', 'binary', invalid: :replace, undef: :replace)
|
|
2047
|
-
end
|
|
2048
|
-
op.split("\n")
|
|
2049
|
-
end
|
|
2050
|
-
|
|
2051
2688
|
# Returns a hash of environment variable overrides for git commands
|
|
2052
2689
|
#
|
|
2053
2690
|
# This method builds a hash of environment variables that control git's behavior,
|
|
@@ -2088,6 +2725,7 @@ module Git
|
|
|
2088
2725
|
'GIT_WORK_TREE' => @git_work_dir,
|
|
2089
2726
|
'GIT_INDEX_FILE' => @git_index_file,
|
|
2090
2727
|
'GIT_SSH' => resolved_git_ssh,
|
|
2728
|
+
'GIT_EDITOR' => 'true', # Use a no-op editor so Git skips interactive editing but continues
|
|
2091
2729
|
'LC_ALL' => 'en_US.UTF-8'
|
|
2092
2730
|
}.merge(additional_overrides)
|
|
2093
2731
|
end
|
|
@@ -2115,154 +2753,85 @@ module Git
|
|
|
2115
2753
|
end
|
|
2116
2754
|
end
|
|
2117
2755
|
|
|
2118
|
-
|
|
2119
|
-
@command_line ||=
|
|
2120
|
-
Git::CommandLine.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
|
|
2121
|
-
end
|
|
2122
|
-
|
|
2123
|
-
# Returns a command line instance without GIT_INDEX_FILE for worktree commands
|
|
2124
|
-
#
|
|
2125
|
-
# Git worktrees manage their own index files and setting GIT_INDEX_FILE
|
|
2126
|
-
# causes corruption of both the main worktree and new worktree indexes.
|
|
2127
|
-
#
|
|
2128
|
-
# @return [Git::CommandLine]
|
|
2129
|
-
# @api private
|
|
2130
|
-
#
|
|
2131
|
-
def worktree_command_line
|
|
2132
|
-
@worktree_command_line ||=
|
|
2133
|
-
Git::CommandLine.new(env_overrides('GIT_INDEX_FILE' => nil), Git::Base.config.binary_path, global_opts,
|
|
2134
|
-
@logger)
|
|
2135
|
-
end
|
|
2136
|
-
|
|
2137
|
-
# @overload worktree_command(*args, **options_hash)
|
|
2138
|
-
# Runs a git worktree command and returns the output
|
|
2756
|
+
# Returns the {Git::CommandLine::Capturing} instance used for capturing execution
|
|
2139
2757
|
#
|
|
2140
|
-
#
|
|
2141
|
-
#
|
|
2758
|
+
# Memoized factory for the capturing execution path. Instantiates
|
|
2759
|
+
# {Git::CommandLine::Capturing} with the current environment, binary path,
|
|
2760
|
+
# global options, and logger.
|
|
2142
2761
|
#
|
|
2143
|
-
#
|
|
2144
|
-
# @param options_hash [Hash] the options to pass to the command
|
|
2145
|
-
#
|
|
2146
|
-
# @return [String] the command's stdout
|
|
2762
|
+
# @return [Git::CommandLine::Capturing]
|
|
2147
2763
|
#
|
|
2148
|
-
# @see #
|
|
2149
|
-
#
|
|
2150
|
-
# @api private
|
|
2764
|
+
# @see Git::CommandLine::Capturing#run
|
|
2151
2765
|
#
|
|
2152
|
-
def
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
extra_options = options_hash.keys - COMMAND_ARG_DEFAULTS.keys
|
|
2157
|
-
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
2158
|
-
|
|
2159
|
-
result = worktree_command_line.run(*, **options_hash)
|
|
2160
|
-
result.stdout
|
|
2766
|
+
def command_line_capturing
|
|
2767
|
+
@command_line_capturing ||=
|
|
2768
|
+
Git::CommandLine::Capturing.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
|
|
2161
2769
|
end
|
|
2162
2770
|
|
|
2163
|
-
#
|
|
2164
|
-
#
|
|
2165
|
-
# Additional args are passed to the command line. They should exclude the 'git'
|
|
2166
|
-
# command itself and global options. Remember to splat the the arguments if given
|
|
2167
|
-
# as an array.
|
|
2168
|
-
#
|
|
2169
|
-
# For example, to run `git log --pretty=oneline`, you would create the array
|
|
2170
|
-
# `args = ['log', '--pretty=oneline']` and call `command(*args)`.
|
|
2171
|
-
#
|
|
2172
|
-
# @param options_hash [Hash] the options to pass to the command
|
|
2173
|
-
# @option options_hash [IO, String, #write, nil] :out the destination for captured stdout
|
|
2174
|
-
# @option options_hash [IO, String, #write, nil] :err the destination for captured stderr
|
|
2175
|
-
# @option options_hash [Boolean] :normalize true to normalize the output encoding to UTF-8
|
|
2176
|
-
# @option options_hash [Boolean] :chomp true to remove trailing newlines from the output
|
|
2177
|
-
# @option options_hash [Boolean] :merge true to merge stdout and stderr into a single output
|
|
2178
|
-
# @option options_hash [String, nil] :chdir the directory to run the command in
|
|
2179
|
-
# @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete
|
|
2180
|
-
#
|
|
2181
|
-
# If timeout is nil, the global timeout from {Git::Config} is used.
|
|
2182
|
-
#
|
|
2183
|
-
# If timeout is zero, the timeout will not be enforced.
|
|
2184
|
-
#
|
|
2185
|
-
# If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
|
|
2186
|
-
#
|
|
2187
|
-
# If the command does not respond to SIGKILL, it will hang this method.
|
|
2188
|
-
#
|
|
2189
|
-
# @see Git::CommandLine#run
|
|
2190
|
-
#
|
|
2191
|
-
# @return [String] the command's stdout (or merged stdout and stderr if `merge`
|
|
2192
|
-
# is true)
|
|
2193
|
-
#
|
|
2194
|
-
# @raise [ArgumentError] if an unknown option is passed
|
|
2771
|
+
# Returns the {Git::CommandLine::Streaming} instance used for streaming execution
|
|
2195
2772
|
#
|
|
2196
|
-
#
|
|
2773
|
+
# Memoized factory for the streaming execution path. Instantiates
|
|
2774
|
+
# {Git::CommandLine::Streaming} with the current environment, binary path,
|
|
2775
|
+
# global options, and logger.
|
|
2197
2776
|
#
|
|
2198
|
-
# @
|
|
2199
|
-
#
|
|
2200
|
-
# @raise [Git::TimeoutError] if the command times out
|
|
2201
|
-
#
|
|
2202
|
-
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
|
|
2203
|
-
#
|
|
2204
|
-
# The exception's `result` attribute is a {Git::CommandLineResult} which will
|
|
2205
|
-
# contain the result of the command including the exit status, stdout, and
|
|
2206
|
-
# stderr.
|
|
2777
|
+
# @return [Git::CommandLine::Streaming]
|
|
2207
2778
|
#
|
|
2208
|
-
# @
|
|
2779
|
+
# @see Git::CommandLine::Streaming#run
|
|
2209
2780
|
#
|
|
2210
|
-
def
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
extra_options = options_hash.keys - COMMAND_ARG_DEFAULTS.keys
|
|
2215
|
-
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
2216
|
-
|
|
2217
|
-
result = command_line.run(*, **options_hash)
|
|
2218
|
-
result.stdout
|
|
2781
|
+
def command_line_streaming
|
|
2782
|
+
@command_line_streaming ||=
|
|
2783
|
+
Git::CommandLine::Streaming.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
|
|
2219
2784
|
end
|
|
2220
2785
|
|
|
2221
|
-
#
|
|
2786
|
+
# Validates the :count option for log commands.
|
|
2222
2787
|
#
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
# @return [Hash] the diff as Hash
|
|
2226
|
-
def diff_as_hash(diff_command, opts = [])
|
|
2227
|
-
# update index before diffing to avoid spurious diffs
|
|
2228
|
-
command('status')
|
|
2229
|
-
command_lines(diff_command, *opts).each_with_object({}) do |line, memo|
|
|
2230
|
-
info, file = split_status_line(line)
|
|
2231
|
-
mode_src, mode_dest, sha_src, sha_dest, type = info.split
|
|
2788
|
+
def validate_log_count_option!(opts)
|
|
2789
|
+
return unless opts[:count] && !opts[:count].is_a?(Integer)
|
|
2232
2790
|
|
|
2233
|
-
|
|
2234
|
-
mode_index: mode_dest, mode_repo: mode_src.to_s[1, 7],
|
|
2235
|
-
path: file, sha_repo: sha_src, sha_index: sha_dest,
|
|
2236
|
-
type: type
|
|
2237
|
-
}
|
|
2238
|
-
end
|
|
2791
|
+
raise ArgumentError, "The log count option must be an Integer but was #{opts[:count].inspect}"
|
|
2239
2792
|
end
|
|
2240
2793
|
|
|
2241
|
-
#
|
|
2794
|
+
# Builds the positional revision range argument(s) from opts for Git::Commands::Log
|
|
2242
2795
|
#
|
|
2243
|
-
# @param [Hash]
|
|
2244
|
-
# @return [Array]
|
|
2245
|
-
def
|
|
2246
|
-
if opts[:
|
|
2247
|
-
|
|
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
|
+
[]
|
|
2248
2805
|
end
|
|
2249
|
-
|
|
2250
|
-
build_args(opts, LOG_OPTION_MAP)
|
|
2251
2806
|
end
|
|
2252
2807
|
|
|
2253
|
-
#
|
|
2808
|
+
# Builds the common keyword options for Git::Commands::Log from opts
|
|
2254
2809
|
#
|
|
2255
|
-
# @param [Hash]
|
|
2256
|
-
# @
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
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"))
|
|
2264
2834
|
end
|
|
2265
|
-
arr_opts
|
|
2266
2835
|
end
|
|
2267
2836
|
|
|
2268
2837
|
def log_or_empty_on_unborn
|
|
@@ -2273,5 +2842,14 @@ module Git
|
|
|
2273
2842
|
|
|
2274
2843
|
[]
|
|
2275
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
|
|
2276
2854
|
end
|
|
2277
2855
|
end
|