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