git 4.3.2 → 5.0.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +67 -2705
- data/.github/pull_request_template.md +3 -1
- data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
- data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
- data/.github/skills/command-implementation/REFERENCE.md +993 -0
- data/.github/skills/command-implementation/SKILL.md +229 -0
- data/.github/skills/command-test-conventions/SKILL.md +660 -0
- data/.github/skills/command-yard-documentation/SKILL.md +426 -0
- data/.github/skills/dependency-management/SKILL.md +72 -0
- data/.github/skills/development-workflow/SKILL.md +506 -0
- data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
- data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
- data/.github/skills/facade-implementation/REFERENCE.md +840 -0
- data/.github/skills/facade-implementation/SKILL.md +260 -0
- data/.github/skills/facade-test-conventions/SKILL.md +380 -0
- data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
- data/.github/skills/make-skill-template/SKILL.md +176 -0
- data/.github/skills/pr-readiness-review/SKILL.md +185 -0
- data/.github/skills/project-context/SKILL.md +313 -0
- data/.github/skills/pull-request-review/SKILL.md +168 -0
- data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
- data/.github/skills/release-management/SKILL.md +125 -0
- data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
- data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
- data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
- data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
- data/.github/skills/reviewing-skills/SKILL.md +189 -0
- data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
- data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
- data/.github/skills/test-debugging/SKILL.md +160 -0
- data/.github/skills/yard-documentation/SKILL.md +793 -0
- data/.github/workflows/continuous_integration.yml +3 -2
- data/.github/workflows/enforce_conventional_commits.yml +1 -1
- data/.github/workflows/experimental_continuous_integration.yml +2 -2
- data/.github/workflows/release.yml +3 -4
- data/.gitignore +8 -0
- data/.husky/pre-commit +13 -0
- data/.release-please-manifest.json +1 -1
- data/.rspec +3 -0
- data/.rubocop.yml +7 -3
- data/.rubocop_todo.yml +23 -5
- data/.yardopts +1 -0
- data/CHANGELOG.md +0 -40
- data/CONTRIBUTING.md +694 -53
- data/README.md +17 -5
- data/Rakefile +61 -9
- data/commitlint.test +4 -0
- data/git.gemspec +14 -8
- data/lib/git/args_builder.rb +0 -8
- data/lib/git/base.rb +486 -410
- data/lib/git/branch.rb +380 -43
- data/lib/git/branch_delete_failure.rb +31 -0
- data/lib/git/branch_delete_result.rb +63 -0
- data/lib/git/branch_info.rb +178 -0
- data/lib/git/branches.rb +130 -24
- data/lib/git/command_line/base.rb +245 -0
- data/lib/git/command_line/capturing.rb +249 -0
- data/lib/git/command_line/result.rb +96 -0
- data/lib/git/command_line/streaming.rb +194 -0
- data/lib/git/command_line.rb +43 -322
- data/lib/git/command_line_result.rb +4 -88
- data/lib/git/commands/add.rb +131 -0
- data/lib/git/commands/am/abort.rb +43 -0
- data/lib/git/commands/am/apply.rb +252 -0
- data/lib/git/commands/am/continue.rb +43 -0
- data/lib/git/commands/am/quit.rb +43 -0
- data/lib/git/commands/am/retry.rb +47 -0
- data/lib/git/commands/am/show_current_patch.rb +64 -0
- data/lib/git/commands/am/skip.rb +42 -0
- data/lib/git/commands/am.rb +33 -0
- data/lib/git/commands/apply.rb +237 -0
- data/lib/git/commands/archive/list_formats.rb +46 -0
- data/lib/git/commands/archive.rb +140 -0
- data/lib/git/commands/arguments.rb +3510 -0
- data/lib/git/commands/base.rb +403 -0
- data/lib/git/commands/branch/copy.rb +94 -0
- data/lib/git/commands/branch/create.rb +173 -0
- data/lib/git/commands/branch/delete.rb +80 -0
- data/lib/git/commands/branch/list.rb +162 -0
- data/lib/git/commands/branch/move.rb +94 -0
- data/lib/git/commands/branch/set_upstream.rb +86 -0
- data/lib/git/commands/branch/show_current.rb +49 -0
- data/lib/git/commands/branch/unset_upstream.rb +57 -0
- data/lib/git/commands/branch.rb +34 -0
- data/lib/git/commands/cat_file/batch.rb +364 -0
- data/lib/git/commands/cat_file/filtered.rb +105 -0
- data/lib/git/commands/cat_file/raw.rb +210 -0
- data/lib/git/commands/cat_file.rb +49 -0
- data/lib/git/commands/checkout/branch.rb +151 -0
- data/lib/git/commands/checkout/files.rb +115 -0
- data/lib/git/commands/checkout.rb +38 -0
- data/lib/git/commands/checkout_index.rb +105 -0
- data/lib/git/commands/clean.rb +100 -0
- data/lib/git/commands/clone.rb +240 -0
- data/lib/git/commands/commit.rb +272 -0
- data/lib/git/commands/commit_tree.rb +100 -0
- data/lib/git/commands/config_option_syntax/add.rb +83 -0
- data/lib/git/commands/config_option_syntax/get.rb +117 -0
- data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
- data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
- data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
- data/lib/git/commands/config_option_syntax/list.rb +107 -0
- data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
- data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
- data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
- data/lib/git/commands/config_option_syntax/set.rb +114 -0
- data/lib/git/commands/config_option_syntax/unset.rb +89 -0
- data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
- data/lib/git/commands/config_option_syntax.rb +56 -0
- data/lib/git/commands/describe.rb +155 -0
- data/lib/git/commands/diff.rb +656 -0
- data/lib/git/commands/diff_files.rb +518 -0
- data/lib/git/commands/diff_index.rb +496 -0
- data/lib/git/commands/fetch.rb +352 -0
- data/lib/git/commands/fsck.rb +136 -0
- data/lib/git/commands/gc.rb +132 -0
- data/lib/git/commands/grep.rb +338 -0
- data/lib/git/commands/init.rb +99 -0
- data/lib/git/commands/log.rb +632 -0
- data/lib/git/commands/ls_files.rb +191 -0
- data/lib/git/commands/ls_remote.rb +155 -0
- data/lib/git/commands/ls_tree.rb +131 -0
- data/lib/git/commands/maintenance/register.rb +75 -0
- data/lib/git/commands/maintenance/run.rb +104 -0
- data/lib/git/commands/maintenance/start.rb +66 -0
- data/lib/git/commands/maintenance/stop.rb +55 -0
- data/lib/git/commands/maintenance/unregister.rb +79 -0
- data/lib/git/commands/maintenance.rb +31 -0
- data/lib/git/commands/merge/abort.rb +44 -0
- data/lib/git/commands/merge/continue.rb +44 -0
- data/lib/git/commands/merge/quit.rb +46 -0
- data/lib/git/commands/merge/start.rb +245 -0
- data/lib/git/commands/merge.rb +28 -0
- data/lib/git/commands/merge_base.rb +86 -0
- data/lib/git/commands/mv.rb +77 -0
- data/lib/git/commands/name_rev.rb +114 -0
- data/lib/git/commands/pull.rb +377 -0
- data/lib/git/commands/push.rb +246 -0
- data/lib/git/commands/read_tree.rb +149 -0
- data/lib/git/commands/remote/add.rb +91 -0
- data/lib/git/commands/remote/get_url.rb +66 -0
- data/lib/git/commands/remote/list.rb +54 -0
- data/lib/git/commands/remote/prune.rb +61 -0
- data/lib/git/commands/remote/remove.rb +52 -0
- data/lib/git/commands/remote/rename.rb +69 -0
- data/lib/git/commands/remote/set_branches.rb +63 -0
- data/lib/git/commands/remote/set_head.rb +82 -0
- data/lib/git/commands/remote/set_url.rb +71 -0
- data/lib/git/commands/remote/set_url_add.rb +61 -0
- data/lib/git/commands/remote/set_url_delete.rb +64 -0
- data/lib/git/commands/remote/show.rb +71 -0
- data/lib/git/commands/remote/update.rb +72 -0
- data/lib/git/commands/remote.rb +42 -0
- data/lib/git/commands/repack.rb +277 -0
- data/lib/git/commands/reset.rb +147 -0
- data/lib/git/commands/rev_parse.rb +297 -0
- data/lib/git/commands/revert/abort.rb +45 -0
- data/lib/git/commands/revert/continue.rb +57 -0
- data/lib/git/commands/revert/quit.rb +47 -0
- data/lib/git/commands/revert/skip.rb +44 -0
- data/lib/git/commands/revert/start.rb +153 -0
- data/lib/git/commands/revert.rb +29 -0
- data/lib/git/commands/rm.rb +114 -0
- data/lib/git/commands/show.rb +632 -0
- data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
- data/lib/git/commands/show_ref/exists.rb +78 -0
- data/lib/git/commands/show_ref/list.rb +145 -0
- data/lib/git/commands/show_ref/verify.rb +120 -0
- data/lib/git/commands/show_ref.rb +42 -0
- data/lib/git/commands/stash/apply.rb +75 -0
- data/lib/git/commands/stash/branch.rb +65 -0
- data/lib/git/commands/stash/clear.rb +41 -0
- data/lib/git/commands/stash/create.rb +58 -0
- data/lib/git/commands/stash/drop.rb +67 -0
- data/lib/git/commands/stash/list.rb +39 -0
- data/lib/git/commands/stash/pop.rb +78 -0
- data/lib/git/commands/stash/push.rb +103 -0
- data/lib/git/commands/stash/show.rb +149 -0
- data/lib/git/commands/stash/store.rb +63 -0
- data/lib/git/commands/stash.rb +38 -0
- data/lib/git/commands/status.rb +169 -0
- data/lib/git/commands/symbolic_ref/delete.rb +68 -0
- data/lib/git/commands/symbolic_ref/read.rb +95 -0
- data/lib/git/commands/symbolic_ref/update.rb +76 -0
- data/lib/git/commands/symbolic_ref.rb +38 -0
- data/lib/git/commands/tag/create.rb +139 -0
- data/lib/git/commands/tag/delete.rb +55 -0
- data/lib/git/commands/tag/list.rb +143 -0
- data/lib/git/commands/tag/verify.rb +71 -0
- data/lib/git/commands/tag.rb +26 -0
- data/lib/git/commands/update_ref/batch.rb +140 -0
- data/lib/git/commands/update_ref/delete.rb +92 -0
- data/lib/git/commands/update_ref/update.rb +106 -0
- data/lib/git/commands/update_ref.rb +42 -0
- data/lib/git/commands/version.rb +52 -0
- data/lib/git/commands/worktree/add.rb +140 -0
- data/lib/git/commands/worktree/list.rb +64 -0
- data/lib/git/commands/worktree/lock.rb +58 -0
- data/lib/git/commands/worktree/management_base.rb +51 -0
- data/lib/git/commands/worktree/move.rb +66 -0
- data/lib/git/commands/worktree/prune.rb +67 -0
- data/lib/git/commands/worktree/remove.rb +63 -0
- data/lib/git/commands/worktree/repair.rb +76 -0
- data/lib/git/commands/worktree/unlock.rb +47 -0
- data/lib/git/commands/worktree.rb +43 -0
- data/lib/git/commands/write_tree.rb +68 -0
- data/lib/git/commands.rb +89 -0
- data/lib/git/detached_head_info.rb +54 -0
- data/lib/git/diff.rb +297 -7
- data/lib/git/diff_file_numstat_info.rb +29 -0
- data/lib/git/diff_file_patch_info.rb +134 -0
- data/lib/git/diff_file_raw_info.rb +127 -0
- data/lib/git/diff_info.rb +169 -0
- data/lib/git/diff_path_status.rb +78 -19
- data/lib/git/diff_result.rb +32 -0
- data/lib/git/diff_stats.rb +59 -14
- data/lib/git/dirstat_info.rb +86 -0
- data/lib/git/errors.rb +65 -2
- data/lib/git/execution_context/global.rb +56 -0
- data/lib/git/execution_context/repository.rb +147 -0
- data/lib/git/execution_context.rb +482 -0
- data/lib/git/file_ref.rb +74 -0
- data/lib/git/fsck_object.rb +9 -9
- data/lib/git/fsck_result.rb +1 -1
- data/lib/git/lib.rb +1606 -1028
- data/lib/git/log.rb +15 -2
- data/lib/git/object.rb +92 -22
- data/lib/git/parsers/branch.rb +224 -0
- data/lib/git/parsers/cat_file.rb +111 -0
- data/lib/git/parsers/diff.rb +585 -0
- data/lib/git/parsers/fsck.rb +133 -0
- data/lib/git/parsers/grep.rb +42 -0
- data/lib/git/parsers/ls_tree.rb +58 -0
- data/lib/git/parsers/stash.rb +208 -0
- data/lib/git/parsers/tag.rb +257 -0
- data/lib/git/remote.rb +133 -9
- data/lib/git/repository/branching.rb +572 -0
- data/lib/git/repository/committing.rb +191 -0
- data/lib/git/repository/configuring.rb +156 -0
- data/lib/git/repository/diffing.rb +775 -0
- data/lib/git/repository/inspecting.rb +153 -0
- data/lib/git/repository/logging.rb +247 -0
- data/lib/git/repository/merging.rb +295 -0
- data/lib/git/repository/object_operations.rb +1101 -0
- data/lib/git/repository/path_resolver.rb +207 -0
- data/lib/git/repository/remote_operations.rb +753 -0
- data/lib/git/repository/shared_private.rb +51 -0
- data/lib/git/repository/staging.rb +390 -0
- data/lib/git/repository/stashing.rb +107 -0
- data/lib/git/repository/status_operations.rb +180 -0
- data/lib/git/repository/worktree_operations.rb +159 -0
- data/lib/git/repository.rb +264 -1
- data/lib/git/stash.rb +85 -4
- data/lib/git/stash_info.rb +104 -0
- data/lib/git/stashes.rb +130 -13
- data/lib/git/status.rb +224 -18
- data/lib/git/tag_delete_failure.rb +31 -0
- data/lib/git/tag_delete_result.rb +63 -0
- data/lib/git/tag_info.rb +105 -0
- data/lib/git/version.rb +109 -2
- data/lib/git/version_constraint.rb +81 -0
- data/lib/git/worktree.rb +120 -5
- data/lib/git/worktrees.rb +107 -7
- data/lib/git.rb +114 -18
- data/redesign/1_architecture_existing.md +54 -18
- data/redesign/2_architecture_redesign.md +365 -46
- data/redesign/3_architecture_implementation.md +1451 -54
- data/tasks/gem_tasks.rake +4 -0
- data/tasks/npm_tasks.rake +7 -0
- data/tasks/rspec.rake +48 -0
- data/tasks/test.rake +13 -1
- data/tasks/yard.rake +34 -7
- metadata +349 -20
- data/lib/git/index.rb +0 -6
- data/lib/git/path.rb +0 -38
- data/lib/git/working_directory.rb +0 -6
- /data/{release-please-config.json → .release-please-config.json} +0 -0
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'git/commands/archive'
|
|
5
|
+
require 'git/object'
|
|
6
|
+
require 'git/commands/cat_file/raw'
|
|
7
|
+
require 'git/commands/grep'
|
|
8
|
+
require 'git/commands/ls_tree'
|
|
9
|
+
require 'git/commands/name_rev'
|
|
10
|
+
require 'git/commands/rev_parse'
|
|
11
|
+
require 'git/commands/show_ref/list'
|
|
12
|
+
require 'git/commands/tag/create'
|
|
13
|
+
require 'git/commands/tag/delete'
|
|
14
|
+
require 'git/commands/tag/list'
|
|
15
|
+
require 'git/parsers/cat_file'
|
|
16
|
+
require 'git/parsers/grep'
|
|
17
|
+
require 'git/parsers/ls_tree'
|
|
18
|
+
require 'git/parsers/tag'
|
|
19
|
+
require 'git/repository/shared_private'
|
|
20
|
+
require 'git/escaped_path'
|
|
21
|
+
require 'tempfile'
|
|
22
|
+
require 'zlib'
|
|
23
|
+
|
|
24
|
+
module Git
|
|
25
|
+
class Repository
|
|
26
|
+
# Facade methods for raw git object store queries
|
|
27
|
+
#
|
|
28
|
+
# Included by {Git::Repository}.
|
|
29
|
+
#
|
|
30
|
+
# @api public
|
|
31
|
+
#
|
|
32
|
+
module ObjectOperations # rubocop:disable Metrics/ModuleLength
|
|
33
|
+
# Returns the raw content of a git object, or streams it into a tempfile
|
|
34
|
+
#
|
|
35
|
+
# Without a block, the full content is buffered in memory and returned as a
|
|
36
|
+
# `String`. With a block, git output is streamed directly to disk without
|
|
37
|
+
# memory buffering — safe for large blobs.
|
|
38
|
+
#
|
|
39
|
+
# @overload cat_file_contents(object)
|
|
40
|
+
# Returns the object's raw content as a string
|
|
41
|
+
#
|
|
42
|
+
# @example Get the contents of a blob
|
|
43
|
+
# repo.cat_file_contents('HEAD:README.md') # => "This is a README file\n"
|
|
44
|
+
#
|
|
45
|
+
# @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
|
|
46
|
+
#
|
|
47
|
+
# @return [String] the raw content of the object
|
|
48
|
+
#
|
|
49
|
+
# @overload cat_file_contents(object, &block)
|
|
50
|
+
# Streams the object's raw content to a temporary file and yields it
|
|
51
|
+
#
|
|
52
|
+
# Git output is written directly to a file on disk without being buffered in
|
|
53
|
+
# memory first, then the file is rewound and yielded to the block. The return
|
|
54
|
+
# value is whatever the block returns.
|
|
55
|
+
#
|
|
56
|
+
# @example Read a large blob without buffering it in memory
|
|
57
|
+
# repo.cat_file_contents('HEAD:large_file.bin') { |f| process(f) }
|
|
58
|
+
#
|
|
59
|
+
# @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
|
|
60
|
+
#
|
|
61
|
+
# @yield [file] the temporary file containing the streamed content,
|
|
62
|
+
# positioned at the start
|
|
63
|
+
#
|
|
64
|
+
# @yieldparam file [File] readable `IO` object positioned at the beginning
|
|
65
|
+
# of the content
|
|
66
|
+
#
|
|
67
|
+
# @yieldreturn [Object] the value to return from this method
|
|
68
|
+
#
|
|
69
|
+
# @return [Object] the value returned by the block
|
|
70
|
+
#
|
|
71
|
+
# @raise [ArgumentError] if `object` starts with a hyphen
|
|
72
|
+
#
|
|
73
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
74
|
+
#
|
|
75
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
|
|
76
|
+
#
|
|
77
|
+
def cat_file_contents(object)
|
|
78
|
+
raise ArgumentError, "Invalid object: '#{object}'" if object&.start_with?('-')
|
|
79
|
+
|
|
80
|
+
return Git::Commands::CatFile::Raw.new(@execution_context).call(object, p: true).stdout unless block_given?
|
|
81
|
+
|
|
82
|
+
# Stream git output directly to a tempfile to avoid buffering large
|
|
83
|
+
# object content in memory when a block is given.
|
|
84
|
+
Tempfile.create do |file|
|
|
85
|
+
file.binmode
|
|
86
|
+
Git::Commands::CatFile::Raw.new(@execution_context).call(object, p: true, out: file)
|
|
87
|
+
file.rewind
|
|
88
|
+
yield file
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the size of a git object in bytes
|
|
93
|
+
#
|
|
94
|
+
# @example Get the size of a commit object
|
|
95
|
+
# repo.cat_file_size('HEAD') #=> 265
|
|
96
|
+
#
|
|
97
|
+
# @example Get the size of a blob by treeish path
|
|
98
|
+
# repo.cat_file_size('HEAD:README.md') #=> 14
|
|
99
|
+
#
|
|
100
|
+
# @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
|
|
101
|
+
#
|
|
102
|
+
# @return [Integer] the object size in bytes
|
|
103
|
+
#
|
|
104
|
+
# @raise [ArgumentError] if `object` starts with a hyphen
|
|
105
|
+
#
|
|
106
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
107
|
+
#
|
|
108
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
|
|
109
|
+
#
|
|
110
|
+
def cat_file_size(object)
|
|
111
|
+
raise ArgumentError, "Invalid object: '#{object}'" if object&.start_with?('-')
|
|
112
|
+
|
|
113
|
+
Git::Commands::CatFile::Raw.new(@execution_context).call(object, s: true).stdout.chomp.to_i
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Returns the type of a git object
|
|
117
|
+
#
|
|
118
|
+
# @example Get the type of a commit reference
|
|
119
|
+
# repo.cat_file_type('HEAD') #=> "commit"
|
|
120
|
+
#
|
|
121
|
+
# @example Get the type of a blob via treeish path
|
|
122
|
+
# repo.cat_file_type('HEAD:README.md') #=> "blob"
|
|
123
|
+
#
|
|
124
|
+
# @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
|
|
125
|
+
#
|
|
126
|
+
# @return [String] the object type — one of `"blob"`, `"commit"`,
|
|
127
|
+
# `"tag"`, or `"tree"`
|
|
128
|
+
#
|
|
129
|
+
# @raise [ArgumentError] if `object` starts with a hyphen
|
|
130
|
+
#
|
|
131
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
132
|
+
#
|
|
133
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
|
|
134
|
+
#
|
|
135
|
+
def cat_file_type(object)
|
|
136
|
+
raise ArgumentError, "Invalid object: '#{object}'" if object&.start_with?('-')
|
|
137
|
+
|
|
138
|
+
Git::Commands::CatFile::Raw.new(@execution_context).call(object, t: true).stdout.chomp
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns parsed commit data for the given git object
|
|
142
|
+
#
|
|
143
|
+
# @example Get commit data for HEAD
|
|
144
|
+
# repo.cat_file_commit('HEAD')
|
|
145
|
+
# # => {
|
|
146
|
+
# # 'sha' => 'HEAD',
|
|
147
|
+
# # 'tree' => 'def5678...',
|
|
148
|
+
# # 'parent' => ['ghi9012...'],
|
|
149
|
+
# # 'author' => 'A U Thor <author@example.com> 1234567890 +0000',
|
|
150
|
+
# # 'committer' => 'A U Thor <author@example.com> 1234567890 +0000',
|
|
151
|
+
# # 'message' => "Initial commit\n"
|
|
152
|
+
# # }
|
|
153
|
+
#
|
|
154
|
+
# @param object [String] the object name (SHA, ref, `HEAD`, etc.)
|
|
155
|
+
#
|
|
156
|
+
# @return [Hash] commit data
|
|
157
|
+
#
|
|
158
|
+
# String-keyed hash with the following keys:
|
|
159
|
+
#
|
|
160
|
+
# * `tree` — the tree SHA
|
|
161
|
+
# * `parent` — Array of parent SHAs (empty for the root commit)
|
|
162
|
+
# * `author` — author identity string and timestamp
|
|
163
|
+
# * `committer` — committer identity string and timestamp
|
|
164
|
+
# * `message` — the commit message (includes trailing newline)
|
|
165
|
+
# * `gpgsig` — the cryptographic signature (signed commits only)
|
|
166
|
+
# * `sha` — the `object` argument as passed by the caller
|
|
167
|
+
#
|
|
168
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
169
|
+
#
|
|
170
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
|
|
171
|
+
#
|
|
172
|
+
def cat_file_commit(object)
|
|
173
|
+
result = Git::Commands::CatFile::Raw.new(@execution_context).call('commit', object)
|
|
174
|
+
Git::Parsers::CatFile.parse_commit(result.stdout.split("\n"), object)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns parsed tag data for the given annotated tag object
|
|
178
|
+
#
|
|
179
|
+
# Does not work with lightweight tags. To list all annotated tags in a
|
|
180
|
+
# repository:
|
|
181
|
+
#
|
|
182
|
+
# ```sh
|
|
183
|
+
# git for-each-ref --format='%(refname:strip=2)' refs/tags | \
|
|
184
|
+
# while read tag; do
|
|
185
|
+
# git cat-file tag "$tag" >/dev/null 2>&1 && echo "$tag"
|
|
186
|
+
# done
|
|
187
|
+
# ```
|
|
188
|
+
#
|
|
189
|
+
# @example Get tag data for an annotated tag
|
|
190
|
+
# repo.cat_file_tag('v1.0')
|
|
191
|
+
# # => {
|
|
192
|
+
# # 'name' => 'v1.0',
|
|
193
|
+
# # 'object' => 'abc1234...',
|
|
194
|
+
# # 'type' => 'commit',
|
|
195
|
+
# # 'tag' => 'v1.0',
|
|
196
|
+
# # 'tagger' => 'A U Thor <author@example.com> 1234567890 +0000',
|
|
197
|
+
# # 'message' => "Release v1.0\n"
|
|
198
|
+
# # }
|
|
199
|
+
#
|
|
200
|
+
# @param object [String] the annotated tag name or SHA
|
|
201
|
+
#
|
|
202
|
+
# @return [Hash] tag data
|
|
203
|
+
#
|
|
204
|
+
# String-keyed hash with the following keys:
|
|
205
|
+
#
|
|
206
|
+
# * `name` — the `object` argument as passed by the caller
|
|
207
|
+
# * `object` — the SHA of the tagged object
|
|
208
|
+
# * `type` — the type of the tagged object (usually `"commit"`)
|
|
209
|
+
# * `tag` — the tag name
|
|
210
|
+
# * `tagger` — tagger identity string and timestamp
|
|
211
|
+
# * `message` — the tag message (includes trailing newline)
|
|
212
|
+
#
|
|
213
|
+
# @raise [ArgumentError] if `object` starts with a hyphen
|
|
214
|
+
#
|
|
215
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
216
|
+
#
|
|
217
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
|
|
218
|
+
#
|
|
219
|
+
def cat_file_tag(object)
|
|
220
|
+
raise ArgumentError, "Invalid object: '#{object}'" if object&.start_with?('-')
|
|
221
|
+
|
|
222
|
+
tdata = Git::Commands::CatFile::Raw.new(@execution_context).call('tag', object).stdout.split("\n")
|
|
223
|
+
Git::Parsers::CatFile.parse_tag(tdata, object)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Resolve a revision specifier to its full object ID
|
|
227
|
+
#
|
|
228
|
+
# Passes the given revision specifier to `git rev-parse` and returns the
|
|
229
|
+
# full object ID.
|
|
230
|
+
#
|
|
231
|
+
# @example Resolve HEAD to its full object ID
|
|
232
|
+
# repo.rev_parse('HEAD') #=> "9b9b31e704c0b85ffdd8d2af2ded85170a5af87d"
|
|
233
|
+
#
|
|
234
|
+
# @example Resolve an abbreviated SHA
|
|
235
|
+
# repo.rev_parse('9b9b31e') #=> "9b9b31e704c0b85ffdd8d2af2ded85170a5af87d"
|
|
236
|
+
#
|
|
237
|
+
# @example Resolve a tree object via rev-parse syntax
|
|
238
|
+
# repo.rev_parse('HEAD^{tree}') #=> "94c827875e2cadb8bc8d4cdd900f19aa9e8634c7"
|
|
239
|
+
#
|
|
240
|
+
# @param objectish [String] the revision specifier to resolve (branch name,
|
|
241
|
+
# tag, abbreviated SHA, refspec, etc.)
|
|
242
|
+
#
|
|
243
|
+
# @return [String] the full object ID of the resolved object
|
|
244
|
+
#
|
|
245
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
246
|
+
#
|
|
247
|
+
# @see https://git-scm.com/docs/git-rev-parse git-rev-parse documentation
|
|
248
|
+
#
|
|
249
|
+
# @see https://git-scm.com/docs/git-rev-parse#_specifying_revisions Valid ways to specify revisions
|
|
250
|
+
#
|
|
251
|
+
def rev_parse(objectish)
|
|
252
|
+
Git::Commands::RevParse.new(@execution_context).call(objectish, '--', revs_only: true).stdout
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns the SHA of a named tag
|
|
256
|
+
#
|
|
257
|
+
# Returns an empty string when the tag does not exist.
|
|
258
|
+
#
|
|
259
|
+
# @example Get the SHA of an existing tag
|
|
260
|
+
# repo.tag_sha('v1.0')
|
|
261
|
+
# #=> "abc1234567890abcdef1234567890abcdef123456"
|
|
262
|
+
#
|
|
263
|
+
# @example Get the SHA of a non-existent tag
|
|
264
|
+
# repo.tag_sha('nonexistent') #=> ""
|
|
265
|
+
#
|
|
266
|
+
# @param tag_name [String] the tag name to look up
|
|
267
|
+
#
|
|
268
|
+
# @return [String] the SHA of the named tag, or an empty string if the
|
|
269
|
+
# tag does not exist
|
|
270
|
+
#
|
|
271
|
+
# @see https://git-scm.com/docs/git-show-ref git-show-ref documentation
|
|
272
|
+
#
|
|
273
|
+
def tag_sha(tag_name)
|
|
274
|
+
tags_dir = File.expand_path(File.join(@execution_context.git_dir, 'refs', 'tags'))
|
|
275
|
+
head = File.expand_path(File.join(tags_dir, tag_name))
|
|
276
|
+
return File.read(head).chomp if head.start_with?("#{tags_dir}#{File::SEPARATOR}") && File.file?(head)
|
|
277
|
+
|
|
278
|
+
Private.show_ref_tag_sha(@execution_context, tag_name)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Returns all recursive entries for a given tree object
|
|
282
|
+
#
|
|
283
|
+
# Equivalent to running `git ls-tree -r <objectish>` and splitting the
|
|
284
|
+
# output on newlines. Each returned line describes a single entry in the
|
|
285
|
+
# tree in the format produced by `git ls-tree`: `<mode> <type> <object>\t<file>`.
|
|
286
|
+
#
|
|
287
|
+
# @example List all files in the tree rooted at HEAD
|
|
288
|
+
# repo.full_tree('HEAD^{tree}')
|
|
289
|
+
# # => [
|
|
290
|
+
# # "100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\tex_dir/ex.txt",
|
|
291
|
+
# # "100644 blob abc1234...\tlib/git.rb"
|
|
292
|
+
# # ]
|
|
293
|
+
#
|
|
294
|
+
# @param objectish [String] the tree SHA or tree-ish specifier to recurse
|
|
295
|
+
# into
|
|
296
|
+
#
|
|
297
|
+
# @return [Array<String>] one entry per path, in the format
|
|
298
|
+
# `<mode> <type> <object>\t<file>`
|
|
299
|
+
#
|
|
300
|
+
# Returns an empty array for an empty tree.
|
|
301
|
+
#
|
|
302
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
303
|
+
#
|
|
304
|
+
# @see https://git-scm.com/docs/git-ls-tree git-ls-tree documentation
|
|
305
|
+
#
|
|
306
|
+
def full_tree(objectish)
|
|
307
|
+
Git::Commands::LsTree.new(@execution_context).call(objectish, r: true).stdout.split("\n")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Returns the number of entries in a tree
|
|
311
|
+
#
|
|
312
|
+
# Runs `git ls-tree -r <objectish>` and counts output lines.
|
|
313
|
+
# This matches `Git::Lib#tree_depth` behavior in the 4.x branch.
|
|
314
|
+
#
|
|
315
|
+
# @example Count entries in the tree rooted at HEAD
|
|
316
|
+
# repo.tree_depth('HEAD^{tree}') #=> 42
|
|
317
|
+
#
|
|
318
|
+
# @param objectish [String] the tree SHA or tree-ish specifier to recurse
|
|
319
|
+
# into
|
|
320
|
+
#
|
|
321
|
+
# @return [Integer] the number of entries in the recursive tree listing
|
|
322
|
+
#
|
|
323
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
324
|
+
#
|
|
325
|
+
# @see https://git-scm.com/docs/git-ls-tree git-ls-tree documentation
|
|
326
|
+
#
|
|
327
|
+
def tree_depth(objectish)
|
|
328
|
+
Git::Commands::LsTree.new(@execution_context).call(objectish, r: true).stdout.each_line.count
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Find the first symbolic name for a commit-ish
|
|
332
|
+
#
|
|
333
|
+
# @example Find the symbolic name for a commit
|
|
334
|
+
# repo.name_rev('abc123') #=> "main~5"
|
|
335
|
+
#
|
|
336
|
+
# @example Find the symbolic name for HEAD
|
|
337
|
+
# repo.name_rev('HEAD') #=> "main"
|
|
338
|
+
#
|
|
339
|
+
# @param commit_ish [String] the commit-ish to find the symbolic name of
|
|
340
|
+
#
|
|
341
|
+
# @return [String, nil] the first symbolic name, or nil if stdout contains
|
|
342
|
+
# fewer than two words
|
|
343
|
+
#
|
|
344
|
+
# @raise [ArgumentError] if commit_ish starts with a hyphen
|
|
345
|
+
#
|
|
346
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
347
|
+
#
|
|
348
|
+
# @see https://git-scm.com/docs/git-name-rev git-name-rev documentation
|
|
349
|
+
#
|
|
350
|
+
def name_rev(commit_ish)
|
|
351
|
+
raise ArgumentError, "Invalid commit_ish: '#{commit_ish}'" if commit_ish&.start_with?('-')
|
|
352
|
+
|
|
353
|
+
Git::Commands::NameRev.new(@execution_context).call(commit_ish).stdout.split[1]
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Option keys accepted by {#ls_tree}
|
|
357
|
+
LS_TREE_ALLOWED_OPTS = %i[recursive path].freeze
|
|
358
|
+
private_constant :LS_TREE_ALLOWED_OPTS
|
|
359
|
+
|
|
360
|
+
# List the objects in a git tree
|
|
361
|
+
#
|
|
362
|
+
# Runs `git ls-tree` against the given sha and returns a Hash of tree
|
|
363
|
+
# entries organised by object type.
|
|
364
|
+
#
|
|
365
|
+
# @example List the top-level tree
|
|
366
|
+
# repo.ls_tree('HEAD')
|
|
367
|
+
# # => { 'blob' => { 'README.md' => { mode: '100644', sha: 'abc...' } },
|
|
368
|
+
# # 'tree' => { 'lib' => { mode: '040000', sha: 'def...' } },
|
|
369
|
+
# # 'commit' => {} }
|
|
370
|
+
#
|
|
371
|
+
# @example List the tree recursively
|
|
372
|
+
# repo.ls_tree('HEAD', recursive: true)
|
|
373
|
+
# # => { 'blob' => { 'lib/git.rb' => { mode: '100644', sha: '...' } }, ... }
|
|
374
|
+
#
|
|
375
|
+
# @example Limit the listing to a path
|
|
376
|
+
# repo.ls_tree('HEAD', path: 'lib/')
|
|
377
|
+
#
|
|
378
|
+
# @param objectish [String] the tree-ish object to list
|
|
379
|
+
#
|
|
380
|
+
# @param opts [Hash] additional options
|
|
381
|
+
#
|
|
382
|
+
# @option opts [Boolean, nil] :recursive (nil) recurse into subtrees
|
|
383
|
+
#
|
|
384
|
+
# @option opts [String, Array<String>] :path (nil) path or array of paths
|
|
385
|
+
# to limit the listing to
|
|
386
|
+
#
|
|
387
|
+
# @return [Hash<String, Hash<String, Hash>>] a three-level Hash keyed by
|
|
388
|
+
# object type (`'blob'`, `'tree'`, `'commit'`), then by filename, then
|
|
389
|
+
# holding `:mode` and `:sha` values
|
|
390
|
+
#
|
|
391
|
+
# @raise [ArgumentError] when unsupported options are provided
|
|
392
|
+
#
|
|
393
|
+
# @raise [Git::FailedError] when git exits with a non-zero exit status
|
|
394
|
+
#
|
|
395
|
+
# @see https://git-scm.com/docs/git-ls-tree git-ls-tree documentation
|
|
396
|
+
#
|
|
397
|
+
def ls_tree(objectish, opts = {})
|
|
398
|
+
SharedPrivate.assert_valid_opts!(LS_TREE_ALLOWED_OPTS, **opts)
|
|
399
|
+
paths = Array(opts[:path]).compact
|
|
400
|
+
r_value = opts[:recursive]
|
|
401
|
+
safe_options = {}
|
|
402
|
+
safe_options[:r] = r_value unless r_value.nil?
|
|
403
|
+
result = Git::Commands::LsTree.new(@execution_context).call(objectish, *paths, **safe_options)
|
|
404
|
+
Git::Parsers::LsTree.parse(result.stdout)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Option keys accepted by {#grep}
|
|
408
|
+
GREP_ALLOWED_OPTS = %i[ignore_case i invert_match v extended_regexp E object].freeze
|
|
409
|
+
private_constant :GREP_ALLOWED_OPTS
|
|
410
|
+
|
|
411
|
+
# Search tracked file contents in a git tree for a pattern
|
|
412
|
+
#
|
|
413
|
+
# Runs `git grep` against the given tree-ish and returns every match as a
|
|
414
|
+
# filename-keyed hash of `[line_number, text]` pairs.
|
|
415
|
+
#
|
|
416
|
+
# @example Search HEAD for a pattern
|
|
417
|
+
# repo.grep('TODO')
|
|
418
|
+
# # => { "HEAD:src/foo.rb" => [[12, "# TODO: fix this"]], ... }
|
|
419
|
+
#
|
|
420
|
+
# @example Limit the search to a path
|
|
421
|
+
# repo.grep('TODO', 'src/')
|
|
422
|
+
#
|
|
423
|
+
# @example Search a specific commit
|
|
424
|
+
# repo.grep('TODO', nil, object: 'abc1234')
|
|
425
|
+
#
|
|
426
|
+
# @example Case-insensitive search
|
|
427
|
+
# repo.grep('todo', nil, ignore_case: true)
|
|
428
|
+
#
|
|
429
|
+
# @param pattern [String] the pattern to search for
|
|
430
|
+
#
|
|
431
|
+
# @param path_limiter [String, Pathname, Array<String, Pathname>, nil]
|
|
432
|
+
# a path or array of paths to limit the search to, or `nil` for no limit
|
|
433
|
+
#
|
|
434
|
+
# @param opts [Hash] additional options for the grep command
|
|
435
|
+
#
|
|
436
|
+
# @option opts [String] :object ('HEAD') the tree-ish to search
|
|
437
|
+
#
|
|
438
|
+
# @option opts [Boolean, nil] :ignore_case (nil) ignore case
|
|
439
|
+
# distinctions in both the pattern and the file contents
|
|
440
|
+
#
|
|
441
|
+
# Alias: :i
|
|
442
|
+
#
|
|
443
|
+
# @option opts [Boolean, nil] :invert_match (nil) select non-matching
|
|
444
|
+
# lines
|
|
445
|
+
#
|
|
446
|
+
# Alias: :v
|
|
447
|
+
#
|
|
448
|
+
# @option opts [Boolean, nil] :extended_regexp (nil) use POSIX extended
|
|
449
|
+
# regular expressions for the pattern
|
|
450
|
+
#
|
|
451
|
+
# Alias: :E
|
|
452
|
+
#
|
|
453
|
+
# @return [Hash<String, Array<Array(Integer, String)>>] a hash mapping
|
|
454
|
+
# each `"treeish:filename"` key to an array of `[line_number, text]`
|
|
455
|
+
# pairs; returns an empty hash when no lines match
|
|
456
|
+
#
|
|
457
|
+
# @raise [ArgumentError] if unsupported options are provided
|
|
458
|
+
#
|
|
459
|
+
# @raise [Git::FailedError] if git exits with a non-zero status and
|
|
460
|
+
# stderr is non-empty (e.g. bad object reference)
|
|
461
|
+
#
|
|
462
|
+
# @see https://git-scm.com/docs/git-grep git-grep documentation
|
|
463
|
+
#
|
|
464
|
+
def grep(pattern, path_limiter = nil, opts = {})
|
|
465
|
+
SharedPrivate.assert_valid_opts!(GREP_ALLOWED_OPTS, **opts)
|
|
466
|
+
opts = opts.dup
|
|
467
|
+
object = opts.delete(:object) || 'HEAD'
|
|
468
|
+
opts[:pathspec] = Array(path_limiter).map(&:to_s) if path_limiter
|
|
469
|
+
result = Git::Commands::Grep.new(@execution_context).call(
|
|
470
|
+
object, pattern:, **opts, no_color: true, line_number: true, null: true
|
|
471
|
+
)
|
|
472
|
+
Private.parse_grep_result(result)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Option keys accepted by {#archive}
|
|
476
|
+
ARCHIVE_ALLOWED_OPTS = %i[prefix remote path format add_gzip].freeze
|
|
477
|
+
private_constant :ARCHIVE_ALLOWED_OPTS
|
|
478
|
+
|
|
479
|
+
# Create an archive of the repository tree and write it to a file
|
|
480
|
+
#
|
|
481
|
+
# Writes the archive content to a file and returns the file path. The
|
|
482
|
+
# default format is `zip`. Pass `format: 'tar'` for an uncompressed tar
|
|
483
|
+
# archive, or `format: 'tgz'` for a gzip-compressed tar archive
|
|
484
|
+
# (equivalent to `format: 'tar'` with `add_gzip: true`).
|
|
485
|
+
#
|
|
486
|
+
# When no `file` path is given, a temporary file is created and its path
|
|
487
|
+
# is returned.
|
|
488
|
+
#
|
|
489
|
+
# **File replacement behavior when `file` is given:**
|
|
490
|
+
#
|
|
491
|
+
# The archive is first written to a staging file in the same directory as
|
|
492
|
+
# `file`. This means write permission is required on the parent directory
|
|
493
|
+
# of `file`, not just on `file` itself. Once the archive is fully written,
|
|
494
|
+
# the staging file atomically replaces `file` via rename.
|
|
495
|
+
#
|
|
496
|
+
# If `file` already exists, only its numeric permission bits are applied to
|
|
497
|
+
# the new archive; ownership, ACLs, and extended attributes are not
|
|
498
|
+
# transferred. If `file` does not exist, the archive receives the standard
|
|
499
|
+
# file creation mode (`0666 & ~umask`). On Windows, `File.chmod` has no
|
|
500
|
+
# effect, so the archive always receives the default creation mode
|
|
501
|
+
# regardless of whether `file` already exists.
|
|
502
|
+
#
|
|
503
|
+
# If `file` is a symlink that does not point to a directory, the symlink
|
|
504
|
+
# itself is replaced by the new archive file rather than writing through
|
|
505
|
+
# the link to its target. A symlink that points to a directory is treated
|
|
506
|
+
# as a directory and rejected with `ArgumentError`.
|
|
507
|
+
#
|
|
508
|
+
# @example Archive HEAD as a zip file
|
|
509
|
+
# repo.archive('HEAD', '/tmp/release.zip') #=> "/tmp/release.zip"
|
|
510
|
+
#
|
|
511
|
+
# @example Archive a tag as a tar file
|
|
512
|
+
# repo.archive('v1.0', '/tmp/release.tar', format: 'tar') #=> "/tmp/release.tar"
|
|
513
|
+
#
|
|
514
|
+
# @example Archive with a path prefix applied to every entry
|
|
515
|
+
# repo.archive('HEAD', '/tmp/out.tar', format: 'tar', prefix: 'myproject/')
|
|
516
|
+
# #=> "/tmp/out.tar"
|
|
517
|
+
#
|
|
518
|
+
# @example Archive a subdirectory only
|
|
519
|
+
# repo.archive('HEAD', '/tmp/src.tar', format: 'tar', path: 'src/')
|
|
520
|
+
# #=> "/tmp/src.tar"
|
|
521
|
+
#
|
|
522
|
+
# @param treeish [String] tree-ish to archive — commit SHA, tag, branch
|
|
523
|
+
# name, or tree SHA
|
|
524
|
+
#
|
|
525
|
+
# @param file [String, nil] (nil) destination file path; when `nil`, a
|
|
526
|
+
# unique temporary file is created and its path is returned
|
|
527
|
+
#
|
|
528
|
+
# @param opts [Hash] archive options
|
|
529
|
+
#
|
|
530
|
+
# @option opts [String] :format ('zip') archive format — `'tar'`, `'zip'`,
|
|
531
|
+
# or `'tgz'`; `'tgz'` is internally converted to `'tar'` with gzip
|
|
532
|
+
# post-processing
|
|
533
|
+
#
|
|
534
|
+
# @option opts [String] :prefix (nil) prefix prepended to every filename
|
|
535
|
+
# in the archive; typically ends with `/`
|
|
536
|
+
#
|
|
537
|
+
# @option opts [String] :path (nil) path within the tree to include in the
|
|
538
|
+
# archive; when given, only files under that path are archived
|
|
539
|
+
#
|
|
540
|
+
# @option opts [String] :remote (nil) retrieve the archive from a remote
|
|
541
|
+
# repository rather than the local one
|
|
542
|
+
#
|
|
543
|
+
# @option opts [Boolean, nil] :add_gzip (nil) apply gzip compression after
|
|
544
|
+
# writing the archive; set automatically when `format: 'tgz'` is given
|
|
545
|
+
#
|
|
546
|
+
# @return [String] path to the written archive file
|
|
547
|
+
#
|
|
548
|
+
# @raise [ArgumentError] if unsupported options are provided
|
|
549
|
+
#
|
|
550
|
+
# @raise [ArgumentError] if `file` is an existing directory
|
|
551
|
+
#
|
|
552
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
553
|
+
#
|
|
554
|
+
# @see https://git-scm.com/docs/git-archive git-archive documentation
|
|
555
|
+
#
|
|
556
|
+
def archive(treeish, file = nil, opts = {})
|
|
557
|
+
SharedPrivate.assert_valid_opts!(ARCHIVE_ALLOWED_OPTS, **opts)
|
|
558
|
+
raise ArgumentError, "#{file.inspect} is a directory" if file && File.directory?(file)
|
|
559
|
+
|
|
560
|
+
tmp = Private.write_archive_tmp(@execution_context, treeish, opts, dest_dir: Private.staging_dir_for(file))
|
|
561
|
+
return tmp unless file
|
|
562
|
+
|
|
563
|
+
Private.atomic_replace(tmp, file)
|
|
564
|
+
file
|
|
565
|
+
rescue StandardError
|
|
566
|
+
FileUtils.rm_f(tmp) if tmp
|
|
567
|
+
raise
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Returns a blob object for the given object reference
|
|
571
|
+
#
|
|
572
|
+
# The returned object is lazy: no git command is invoked until a property
|
|
573
|
+
# (e.g. {Git::Object::AbstractObject#sha}, {Git::Object::AbstractObject#contents})
|
|
574
|
+
# is accessed on the result.
|
|
575
|
+
#
|
|
576
|
+
# @example Get a blob from a treeish path
|
|
577
|
+
# repo.gblob('HEAD:README.md')
|
|
578
|
+
# #=> #<Git::Object::Blob ...>
|
|
579
|
+
#
|
|
580
|
+
# @param objectish [String] the object name (SHA, treeish path, ref, etc.)
|
|
581
|
+
#
|
|
582
|
+
# @return [Git::Object::Blob] the blob object
|
|
583
|
+
#
|
|
584
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
|
|
585
|
+
#
|
|
586
|
+
def gblob(objectish)
|
|
587
|
+
Git::Object.new(self, objectish, 'blob')
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# Returns a commit object for the given object reference
|
|
591
|
+
#
|
|
592
|
+
# The returned object is lazy: no git command is invoked until a property
|
|
593
|
+
# (e.g. {Git::Object::AbstractObject#sha}, {Git::Object::Commit#message})
|
|
594
|
+
# is accessed on the result.
|
|
595
|
+
#
|
|
596
|
+
# @example Get a commit by symbolic ref
|
|
597
|
+
# repo.gcommit('HEAD')
|
|
598
|
+
# #=> #<Git::Object::Commit ...>
|
|
599
|
+
#
|
|
600
|
+
# @example Get a commit by abbreviated SHA
|
|
601
|
+
# repo.gcommit('abc1234')
|
|
602
|
+
# #=> #<Git::Object::Commit ...>
|
|
603
|
+
#
|
|
604
|
+
# @param objectish [String] the object name (SHA, branch, tag, refspec, etc.)
|
|
605
|
+
#
|
|
606
|
+
# @return [Git::Object::Commit] the commit object
|
|
607
|
+
#
|
|
608
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
|
|
609
|
+
#
|
|
610
|
+
def gcommit(objectish)
|
|
611
|
+
Git::Object.new(self, objectish, 'commit')
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Returns a tree object for the given object reference
|
|
615
|
+
#
|
|
616
|
+
# The returned object is lazy: no git command is invoked until a property
|
|
617
|
+
# (e.g. {Git::Object::AbstractObject#sha}, {Git::Object::Tree#children})
|
|
618
|
+
# is accessed on the result.
|
|
619
|
+
#
|
|
620
|
+
# @example Get the root tree for the current HEAD
|
|
621
|
+
# repo.gtree('HEAD^{tree}')
|
|
622
|
+
# #=> #<Git::Object::Tree ...>
|
|
623
|
+
#
|
|
624
|
+
# @param objectish [String] the object name (SHA, treeish specifier, etc.)
|
|
625
|
+
#
|
|
626
|
+
# @return [Git::Object::Tree] the tree object
|
|
627
|
+
#
|
|
628
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
|
|
629
|
+
#
|
|
630
|
+
def gtree(objectish)
|
|
631
|
+
Git::Object.new(self, objectish, 'tree')
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# Returns a tag object for the given tag name
|
|
635
|
+
#
|
|
636
|
+
# Returns a {Git::Object::Tag} for `tag_name`. The returned object is
|
|
637
|
+
# either an annotated or a lightweight tag depending on the underlying
|
|
638
|
+
# ref type.
|
|
639
|
+
#
|
|
640
|
+
# @example Get a tag object
|
|
641
|
+
# repo.tag('v1.0')
|
|
642
|
+
# #=> #<Git::Object::Tag name="v1.0" ...>
|
|
643
|
+
#
|
|
644
|
+
# @param tag_name [String] the name of the tag
|
|
645
|
+
#
|
|
646
|
+
# @return [Git::Object::Tag] the tag object
|
|
647
|
+
#
|
|
648
|
+
# @raise [Git::UnexpectedResultError] if `tag_name` does not name an
|
|
649
|
+
# existing tag
|
|
650
|
+
#
|
|
651
|
+
# @raise [Git::FailedError] if the underlying `git show-ref` invocation
|
|
652
|
+
# exits with an unexpected status (i.e., outside the allowed 0..1 range)
|
|
653
|
+
#
|
|
654
|
+
def tag(tag_name)
|
|
655
|
+
Git::Object::Tag.new(self, tag_name)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# Returns the appropriate git object for the given object reference
|
|
659
|
+
#
|
|
660
|
+
# Runs `git cat-file -t` to determine the object type, then constructs
|
|
661
|
+
# and returns the corresponding `Git::Object::*` subclass instance.
|
|
662
|
+
#
|
|
663
|
+
# @example Get a commit object from HEAD
|
|
664
|
+
# repo.object('HEAD')
|
|
665
|
+
# #=> #<Git::Object::Commit ...>
|
|
666
|
+
#
|
|
667
|
+
# @example Get a blob from a treeish path
|
|
668
|
+
# repo.object('HEAD:README.md')
|
|
669
|
+
# #=> #<Git::Object::Blob ...>
|
|
670
|
+
#
|
|
671
|
+
# @param objectish [String] the object name (SHA, ref, treeish path, etc.)
|
|
672
|
+
#
|
|
673
|
+
# @return [Git::Object::Blob, Git::Object::Commit, Git::Object::Tree] the
|
|
674
|
+
# git object for the given reference
|
|
675
|
+
#
|
|
676
|
+
# @raise [ArgumentError] if `objectish` starts with a hyphen
|
|
677
|
+
#
|
|
678
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
679
|
+
#
|
|
680
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
|
|
681
|
+
#
|
|
682
|
+
def object(objectish)
|
|
683
|
+
Git::Object.new(self, objectish)
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Returns all tags in the repository as tag objects
|
|
687
|
+
#
|
|
688
|
+
# Runs `git tag --list` with a machine-readable format, parses the output,
|
|
689
|
+
# and returns a {Git::Object::Tag} for each tag name.
|
|
690
|
+
#
|
|
691
|
+
# @example List the names of all tags
|
|
692
|
+
# repo.tags.map(&:name) #=> ["v1.0.0", "v2.0.0"]
|
|
693
|
+
#
|
|
694
|
+
# @example No tags exist
|
|
695
|
+
# repo.tags #=> []
|
|
696
|
+
#
|
|
697
|
+
# @return [Array<Git::Object::Tag>] one tag object per tag in the
|
|
698
|
+
# repository; empty when there are none
|
|
699
|
+
#
|
|
700
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
701
|
+
#
|
|
702
|
+
def tags
|
|
703
|
+
result = Git::Commands::Tag::List.new(@execution_context).call(format: Git::Parsers::Tag::FORMAT_STRING)
|
|
704
|
+
Git::Parsers::Tag.parse_list(result.stdout).map { |info| tag(info.name) }
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Option keys accepted by {#add_tag}
|
|
708
|
+
ADD_TAG_ALLOWED_OPTS = %i[
|
|
709
|
+
annotate a sign s no_sign local_user u force f message m file F
|
|
710
|
+
edit e no_edit trailer cleanup create_reflog
|
|
711
|
+
].freeze
|
|
712
|
+
private_constant :ADD_TAG_ALLOWED_OPTS
|
|
713
|
+
|
|
714
|
+
# Create a new tag
|
|
715
|
+
#
|
|
716
|
+
# @overload add_tag(name, options = {})
|
|
717
|
+
#
|
|
718
|
+
# @example Create a lightweight tag on HEAD
|
|
719
|
+
# repo.add_tag('v1.0.0')
|
|
720
|
+
#
|
|
721
|
+
# @example Create an annotated tag on HEAD
|
|
722
|
+
# repo.add_tag('v1.0.0', annotate: true, message: 'Release 1.0.0')
|
|
723
|
+
#
|
|
724
|
+
# @example Replace an existing tag on HEAD
|
|
725
|
+
# repo.add_tag('v1.0.0', force: true)
|
|
726
|
+
#
|
|
727
|
+
# @param name [String] the name of the tag to create
|
|
728
|
+
#
|
|
729
|
+
# @param options [Hash] options for creating the tag
|
|
730
|
+
#
|
|
731
|
+
# @option options [Boolean, nil] :annotate (nil) make an unsigned,
|
|
732
|
+
# annotated tag object; requires `:message` or `:file` (alias: `:a`)
|
|
733
|
+
#
|
|
734
|
+
# @option options [Boolean, nil] :a (nil) alias for `:annotate`
|
|
735
|
+
#
|
|
736
|
+
# @option options [Boolean, nil] :sign (nil) make a GPG-signed tag;
|
|
737
|
+
# requires `:message` or `:file` (alias: `:s`)
|
|
738
|
+
#
|
|
739
|
+
# @option options [Boolean, nil] :s (nil) alias for `:sign`
|
|
740
|
+
#
|
|
741
|
+
# @option options [Boolean, nil] :no_sign (nil) override `tag.gpgSign`
|
|
742
|
+
# config to disable signing
|
|
743
|
+
#
|
|
744
|
+
# @option options [String] :local_user (nil) make a signed tag using the
|
|
745
|
+
# given key (alias: `:u`)
|
|
746
|
+
#
|
|
747
|
+
# @option options [String] :u (nil) alias for `:local_user`
|
|
748
|
+
#
|
|
749
|
+
# @option options [Boolean, nil] :force (nil) replace an existing tag with
|
|
750
|
+
# the given name instead of failing (alias: `:f`)
|
|
751
|
+
#
|
|
752
|
+
# @option options [Boolean, nil] :f (nil) alias for `:force`
|
|
753
|
+
#
|
|
754
|
+
# @option options [String] :message (nil) use the given message as the tag
|
|
755
|
+
# message (alias: `:m`)
|
|
756
|
+
#
|
|
757
|
+
# @option options [String] :m (nil) alias for `:message`
|
|
758
|
+
#
|
|
759
|
+
# @option options [String] :file (nil) take the tag message from the given
|
|
760
|
+
# file; use `-` to read from standard input (alias: `:F`)
|
|
761
|
+
#
|
|
762
|
+
# @option options [String] :F (nil) alias for `:file`
|
|
763
|
+
#
|
|
764
|
+
# @option options [Boolean, nil] :edit (nil) open an editor to further edit
|
|
765
|
+
# the tag message (alias: `:e`)
|
|
766
|
+
#
|
|
767
|
+
# @option options [Boolean, nil] :e (nil) alias for `:edit`
|
|
768
|
+
#
|
|
769
|
+
# @option options [Boolean, nil] :no_edit (nil) suppress the editor
|
|
770
|
+
#
|
|
771
|
+
# @option options [Hash, Array<Array>] :trailer (nil) add trailers to the
|
|
772
|
+
# tag message
|
|
773
|
+
#
|
|
774
|
+
# @option options [String] :cleanup (nil) set how the tag message is
|
|
775
|
+
# cleaned up; one of `verbatim`, `whitespace`, or `strip`
|
|
776
|
+
#
|
|
777
|
+
# @option options [Boolean, nil] :create_reflog (nil) create a reflog for
|
|
778
|
+
# the tag
|
|
779
|
+
#
|
|
780
|
+
# @return [Git::Object::Tag] the newly created tag
|
|
781
|
+
#
|
|
782
|
+
# @overload add_tag(name, target, options = {})
|
|
783
|
+
#
|
|
784
|
+
# @example Create a lightweight tag on a specific commit
|
|
785
|
+
# repo.add_tag('v1.0.0', 'abc123')
|
|
786
|
+
#
|
|
787
|
+
# @example Create an annotated tag on a specific commit
|
|
788
|
+
# repo.add_tag('v1.0.0', 'abc123', annotate: true, message: 'Release 1.0.0')
|
|
789
|
+
#
|
|
790
|
+
# @param name [String] the name of the tag to create
|
|
791
|
+
#
|
|
792
|
+
# @param target [String] the object to tag (commit SHA, branch name, etc.)
|
|
793
|
+
#
|
|
794
|
+
# @param options [Hash] options for creating the tag (same keys as the
|
|
795
|
+
# first overload)
|
|
796
|
+
#
|
|
797
|
+
# @return [Git::Object::Tag] the newly created tag
|
|
798
|
+
#
|
|
799
|
+
# @overload add_tag(name, delete: true)
|
|
800
|
+
#
|
|
801
|
+
# @deprecated Use {#delete_tag} instead.
|
|
802
|
+
#
|
|
803
|
+
# @example Delete a tag (deprecated)
|
|
804
|
+
# repo.add_tag('v1.0.0', d: true)
|
|
805
|
+
#
|
|
806
|
+
# @param name [String] the name of the tag to delete
|
|
807
|
+
#
|
|
808
|
+
# @option options [Boolean, nil] :d (nil) delete the named tag
|
|
809
|
+
# (alias: `:delete`); deprecated — use {#delete_tag} instead
|
|
810
|
+
#
|
|
811
|
+
# @option options [Boolean, nil] :delete (nil) delete the named tag
|
|
812
|
+
# (alias: `:d`); deprecated — use {#delete_tag} instead
|
|
813
|
+
#
|
|
814
|
+
# @return [String] git's stdout from the delete
|
|
815
|
+
#
|
|
816
|
+
# @raise [ArgumentError] if a target is also provided
|
|
817
|
+
#
|
|
818
|
+
# @raise [ArgumentError] if options other than `:d`/`:delete` are also
|
|
819
|
+
# provided
|
|
820
|
+
#
|
|
821
|
+
# @raise [ArgumentError] if unsupported options are provided
|
|
822
|
+
#
|
|
823
|
+
# @raise [ArgumentError] if an annotated or signed tag is requested without
|
|
824
|
+
# a message
|
|
825
|
+
#
|
|
826
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
827
|
+
#
|
|
828
|
+
def add_tag(name, *options)
|
|
829
|
+
opts = options.last.is_a?(Hash) ? options.pop : {}
|
|
830
|
+
target = options.first
|
|
831
|
+
|
|
832
|
+
return Private.add_tag_delete_deprecated(self, name, target, opts) if opts[:d] || opts[:delete]
|
|
833
|
+
|
|
834
|
+
opts = opts.except(:d, :delete)
|
|
835
|
+
SharedPrivate.assert_valid_opts!(ADD_TAG_ALLOWED_OPTS, **opts)
|
|
836
|
+
Private.validate_tag_options!(opts)
|
|
837
|
+
Git::Commands::Tag::Create.new(@execution_context).call(name, target, **opts)
|
|
838
|
+
tag(name)
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
# Delete a tag
|
|
842
|
+
#
|
|
843
|
+
# @example Delete a tag
|
|
844
|
+
# repo.delete_tag('v1.0.0')
|
|
845
|
+
#
|
|
846
|
+
# @param name [String] the name of the tag to delete
|
|
847
|
+
#
|
|
848
|
+
# @return [String] git's stdout from the delete
|
|
849
|
+
#
|
|
850
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
851
|
+
#
|
|
852
|
+
def delete_tag(name)
|
|
853
|
+
result = Git::Commands::Tag::Delete.new(@execution_context).call(name)
|
|
854
|
+
raise Git::FailedError, result if result.status.exitstatus.positive?
|
|
855
|
+
|
|
856
|
+
result.stdout
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# Private helpers
|
|
860
|
+
#
|
|
861
|
+
# @api private
|
|
862
|
+
#
|
|
863
|
+
module Private
|
|
864
|
+
module_function
|
|
865
|
+
|
|
866
|
+
# Validate that a message is present when an annotated or signed tag is
|
|
867
|
+
# requested
|
|
868
|
+
#
|
|
869
|
+
# @param opts [Hash] the tag-creation options
|
|
870
|
+
#
|
|
871
|
+
# @return [void]
|
|
872
|
+
#
|
|
873
|
+
# @raise [ArgumentError] when an annotated or signed tag is requested
|
|
874
|
+
# without a `:message`/`:m`/`:file`/`:F` value
|
|
875
|
+
#
|
|
876
|
+
def validate_tag_options!(opts)
|
|
877
|
+
needs_message = %i[a annotate s sign u local_user].any? { |k| opts[k] }
|
|
878
|
+
has_message = opts[:m] || opts[:message] || opts[:F] || opts[:file]
|
|
879
|
+
|
|
880
|
+
return unless needs_message && !has_message
|
|
881
|
+
|
|
882
|
+
raise ArgumentError, 'Cannot create an annotated or signed tag without a message.'
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Handle the deprecated :d/:delete option on add_tag
|
|
886
|
+
#
|
|
887
|
+
# Issues a deprecation warning and delegates to delete_tag. Raises
|
|
888
|
+
# ArgumentError if a target or incompatible options are also supplied.
|
|
889
|
+
#
|
|
890
|
+
# @param facade [ObjectOperations] the calling facade instance
|
|
891
|
+
# @param name [String] tag name
|
|
892
|
+
# @param target [String, nil] target argument (must be nil)
|
|
893
|
+
# @param opts [Hash] options hash (must contain only :d/:delete)
|
|
894
|
+
#
|
|
895
|
+
# @return [String] stdout from delete_tag
|
|
896
|
+
#
|
|
897
|
+
# @api private
|
|
898
|
+
#
|
|
899
|
+
def add_tag_delete_deprecated(facade, name, target, opts)
|
|
900
|
+
Git::Deprecation.warn(
|
|
901
|
+
'Passing :d or :delete to add_tag is deprecated and will be ' \
|
|
902
|
+
'removed in a future version. Use delete_tag instead.'
|
|
903
|
+
)
|
|
904
|
+
raise ArgumentError, 'Cannot pass a target when using the :d/:delete option.' if target
|
|
905
|
+
|
|
906
|
+
extra = opts.keys - %i[d delete]
|
|
907
|
+
raise ArgumentError, "Cannot combine :d/:delete with other options: #{extra.join(', ')}" unless extra.empty?
|
|
908
|
+
|
|
909
|
+
facade.delete_tag(name)
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def show_ref_tag_sha(execution_context, tag_name)
|
|
913
|
+
ref = "refs/tags/#{tag_name}"
|
|
914
|
+
result = Git::Commands::ShowRef::List.new(execution_context).call(ref)
|
|
915
|
+
return '' if result.status.exitstatus == 1
|
|
916
|
+
|
|
917
|
+
line = result.stdout.lines.find { |l| l.split[1] == ref }
|
|
918
|
+
line ? line.split[0] : ''
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
def parse_grep_result(result)
|
|
922
|
+
exitstatus = result.status.exitstatus
|
|
923
|
+
return {} if exitstatus == 1 && result.stderr.empty?
|
|
924
|
+
raise Git::FailedError, result if exitstatus == 1
|
|
925
|
+
|
|
926
|
+
Git::Parsers::Grep.parse(result.stdout)
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
# Resolve the staging directory for a git archive temp file
|
|
930
|
+
#
|
|
931
|
+
# Always returns `Dir.tmpdir` when `file` is nil, or the parent
|
|
932
|
+
# directory of `file` otherwise. Staging the temp file in the same
|
|
933
|
+
# directory as the destination keeps both paths on the same filesystem
|
|
934
|
+
# so that {#atomic_replace} can use an atomic rename that
|
|
935
|
+
# requires no extra disk space.
|
|
936
|
+
#
|
|
937
|
+
# @param file [String, nil] the explicit destination path, or nil
|
|
938
|
+
#
|
|
939
|
+
# @return [String] directory path to pass to `Tempfile.create`
|
|
940
|
+
#
|
|
941
|
+
# @api private
|
|
942
|
+
#
|
|
943
|
+
def staging_dir_for(file)
|
|
944
|
+
return Dir.tmpdir unless file
|
|
945
|
+
|
|
946
|
+
File.dirname(File.expand_path(file))
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
# Write a git archive to a fresh temporary file and return its path
|
|
950
|
+
#
|
|
951
|
+
# Always writes to a new temporary file so that on error the caller's
|
|
952
|
+
# destination file is never truncated. Format and gzip post-processing
|
|
953
|
+
# are determined from `opts` via {#parse_archive_format_options}.
|
|
954
|
+
#
|
|
955
|
+
# @param execution_context [Git::ExecutionContext] for the git command
|
|
956
|
+
# @param treeish [String] tree-ish passed to `git archive`
|
|
957
|
+
# @param opts [Hash] caller-supplied options (read-only)
|
|
958
|
+
# @param dest_dir [String] directory for the staging temp file; use
|
|
959
|
+
# {#staging_dir_for} to select the optimal directory for the destination
|
|
960
|
+
#
|
|
961
|
+
# @return [String] path to the populated temporary file
|
|
962
|
+
#
|
|
963
|
+
# @api private
|
|
964
|
+
#
|
|
965
|
+
def write_archive_tmp(execution_context, treeish, opts, dest_dir: Dir.tmpdir)
|
|
966
|
+
format, gzip = parse_archive_format_options(opts)
|
|
967
|
+
tmp_file = create_archive_tempfile(execution_context, treeish, opts, format, dest_dir)
|
|
968
|
+
apply_gzip(tmp_file.path) if gzip
|
|
969
|
+
tmp_file.path
|
|
970
|
+
rescue StandardError
|
|
971
|
+
tmp_file.close unless tmp_file.nil? || tmp_file.closed?
|
|
972
|
+
FileUtils.rm_f(tmp_file.path) if tmp_file
|
|
973
|
+
raise
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
# Create a staging file, write the archive into it, close it, and return it
|
|
977
|
+
#
|
|
978
|
+
# Uses `Tempfile.create` (not `Tempfile.new`) so that no GC finalizer is
|
|
979
|
+
# registered on the returned object — the file path remains valid after this
|
|
980
|
+
# method returns and after the caller stores only the path string.
|
|
981
|
+
#
|
|
982
|
+
# @param execution_context [Git::ExecutionContext] for the git command
|
|
983
|
+
# @param treeish [String] tree-ish passed to `git archive`
|
|
984
|
+
# @param opts [Hash] caller-supplied options (read-only; used for :prefix,
|
|
985
|
+
# :remote, and :path)
|
|
986
|
+
# @param format [String] archive format string (e.g. `'zip'` or `'tar'`)
|
|
987
|
+
# @param dest_dir [String] directory in which to create the temp file
|
|
988
|
+
#
|
|
989
|
+
# @return [File] the closed file containing the archive
|
|
990
|
+
#
|
|
991
|
+
# @api private
|
|
992
|
+
#
|
|
993
|
+
def create_archive_tempfile(execution_context, treeish, opts, format, dest_dir)
|
|
994
|
+
tmp_file = Tempfile.create('archive', dest_dir).tap(&:binmode)
|
|
995
|
+
run_archive_command(execution_context, treeish, opts, format, tmp_file)
|
|
996
|
+
tmp_file.close
|
|
997
|
+
tmp_file
|
|
998
|
+
rescue StandardError
|
|
999
|
+
tmp_file&.close
|
|
1000
|
+
FileUtils.rm_f(tmp_file.path) if tmp_file
|
|
1001
|
+
raise
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
# Invoke `git archive` and stream output into `tmp_file`
|
|
1005
|
+
#
|
|
1006
|
+
# @param execution_context [Git::ExecutionContext] for the git command
|
|
1007
|
+
# @param treeish [String] tree-ish passed to `git archive`
|
|
1008
|
+
# @param opts [Hash] caller-supplied options (read-only; used for :prefix,
|
|
1009
|
+
# :remote, and :path)
|
|
1010
|
+
# @param format [String] archive format to pass to `git archive --format`
|
|
1011
|
+
# @param tmp_file [File] open, binary-mode IO to write archive data to
|
|
1012
|
+
#
|
|
1013
|
+
# @return [Git::CommandLineResult] the result of the git command
|
|
1014
|
+
#
|
|
1015
|
+
# @api private
|
|
1016
|
+
#
|
|
1017
|
+
def run_archive_command(execution_context, treeish, opts, format, tmp_file)
|
|
1018
|
+
command_opts = opts.slice(:prefix, :remote).merge(format: format)
|
|
1019
|
+
path_args = opts[:path] ? [opts[:path]] : []
|
|
1020
|
+
Git::Commands::Archive.new(execution_context).call(treeish, *path_args, **command_opts, out: tmp_file)
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
# Atomically rename the staging file `src` to `dest`, replacing any
|
|
1024
|
+
# existing file at `dest`. Both paths must be on the same filesystem
|
|
1025
|
+
# (guaranteed when `src` is created by {#staging_dir_for}).
|
|
1026
|
+
#
|
|
1027
|
+
# Before the rename, the staging file's permissions are set to the
|
|
1028
|
+
# existing file's numeric mode (if `dest` already existed) or to
|
|
1029
|
+
# `0666 & ~umask` (standard creation mode) for new files. The chmod
|
|
1030
|
+
# is applied to `src` before the rename so that, if chmod fails, `src`
|
|
1031
|
+
# is still present and can be cleaned up by the rescue. Only the
|
|
1032
|
+
# numeric permission bits are carried over; ownership, ACLs, and
|
|
1033
|
+
# extended attributes from an existing `dest` are not preserved.
|
|
1034
|
+
#
|
|
1035
|
+
# If `dest` is a symlink, the symlink itself is replaced by the renamed
|
|
1036
|
+
# staging file rather than writing through the link to its target.
|
|
1037
|
+
#
|
|
1038
|
+
# @param src [String] staging file path to rename; removed on success
|
|
1039
|
+
# @param dest [String] destination file path
|
|
1040
|
+
#
|
|
1041
|
+
# @return [void]
|
|
1042
|
+
#
|
|
1043
|
+
# @api private
|
|
1044
|
+
#
|
|
1045
|
+
def atomic_replace(src, dest)
|
|
1046
|
+
mode = File.exist?(dest) ? (File.stat(dest).mode & 0o777) : (0o666 & ~File.umask)
|
|
1047
|
+
File.chmod(mode, src)
|
|
1048
|
+
File.rename(src, dest)
|
|
1049
|
+
rescue StandardError
|
|
1050
|
+
FileUtils.rm_f(src)
|
|
1051
|
+
raise
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
# Determine the archive format and whether to apply gzip post-processing
|
|
1055
|
+
#
|
|
1056
|
+
# The `tgz` pseudo-format is not understood by `git archive` directly;
|
|
1057
|
+
# it is converted to `tar` and the gzip flag is set so that {#archive}
|
|
1058
|
+
# applies gzip compression after the archive is written.
|
|
1059
|
+
#
|
|
1060
|
+
# @param opts [Hash] caller-supplied options hash (read-only)
|
|
1061
|
+
#
|
|
1062
|
+
# @return [Array(String, Boolean)] a two-element array `[format, gzip]`
|
|
1063
|
+
#
|
|
1064
|
+
# `format` is the string to pass to `git archive --format`; `gzip` is
|
|
1065
|
+
# `true` when the caller should apply gzip post-processing after writing
|
|
1066
|
+
# the archive.
|
|
1067
|
+
#
|
|
1068
|
+
# @api private
|
|
1069
|
+
#
|
|
1070
|
+
def parse_archive_format_options(opts)
|
|
1071
|
+
format = opts[:format] || 'zip'
|
|
1072
|
+
gzip = opts[:add_gzip] == true || format == 'tgz'
|
|
1073
|
+
[format == 'tgz' ? 'tar' : format, gzip]
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
# Apply gzip compression to the given file in place
|
|
1077
|
+
#
|
|
1078
|
+
# Streams from the source file through a {Zlib::GzipWriter} into a sibling
|
|
1079
|
+
# temporary file, then replaces the original. Peak memory is proportional
|
|
1080
|
+
# to the stream buffer rather than the full archive size.
|
|
1081
|
+
#
|
|
1082
|
+
# @param file [String] path to the file to compress in place
|
|
1083
|
+
#
|
|
1084
|
+
# @return [void]
|
|
1085
|
+
#
|
|
1086
|
+
# @api private
|
|
1087
|
+
#
|
|
1088
|
+
def apply_gzip(file)
|
|
1089
|
+
gz_tmp = Tempfile.create('archive_gz', File.dirname(file)).tap(&:close).path
|
|
1090
|
+
Zlib::GzipWriter.open(gz_tmp) { |gz| File.open(file, 'rb') { |f| IO.copy_stream(f, gz) } }
|
|
1091
|
+
FileUtils.rm_f(file)
|
|
1092
|
+
File.rename(gz_tmp, file)
|
|
1093
|
+
rescue StandardError
|
|
1094
|
+
FileUtils.rm_f(gz_tmp) if gz_tmp
|
|
1095
|
+
raise
|
|
1096
|
+
end
|
|
1097
|
+
end
|
|
1098
|
+
private_constant :Private
|
|
1099
|
+
end
|
|
1100
|
+
end
|
|
1101
|
+
end
|