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