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,775 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'git/commands/diff'
|
|
5
|
+
require 'git/commands/diff_files'
|
|
6
|
+
require 'git/commands/diff_index'
|
|
7
|
+
require 'git/commands/status'
|
|
8
|
+
require 'git/diff'
|
|
9
|
+
require 'git/diff_path_status'
|
|
10
|
+
require 'git/diff_stats'
|
|
11
|
+
require 'git/escaped_path'
|
|
12
|
+
require 'git/repository/shared_private'
|
|
13
|
+
|
|
14
|
+
module Git
|
|
15
|
+
class Repository
|
|
16
|
+
# Facade methods for comparing commits and trees using `git diff`
|
|
17
|
+
#
|
|
18
|
+
# Included by {Git::Repository}.
|
|
19
|
+
#
|
|
20
|
+
# @api public
|
|
21
|
+
#
|
|
22
|
+
module Diffing
|
|
23
|
+
# Option keys accepted by {#diff_full}
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<Symbol>]
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
#
|
|
29
|
+
DIFF_FULL_ALLOWED_OPTS = %i[path_limiter].freeze
|
|
30
|
+
private_constant :DIFF_FULL_ALLOWED_OPTS
|
|
31
|
+
|
|
32
|
+
# Returns the full unified diff patch text between two trees
|
|
33
|
+
#
|
|
34
|
+
# Compares (1) two commits, (2) a commit against the working tree, or (3) the
|
|
35
|
+
# index against the working tree using `git diff -p`, and returns the raw
|
|
36
|
+
# unified diff patch output.
|
|
37
|
+
#
|
|
38
|
+
# **Comparing two commits**
|
|
39
|
+
#
|
|
40
|
+
# When both obj1 and obj2 are provided, the comparison is between those two
|
|
41
|
+
# refs (commits, tags, branches, etc.).
|
|
42
|
+
#
|
|
43
|
+
# **Comparing a commit against the working tree**
|
|
44
|
+
#
|
|
45
|
+
# When only obj1 is provided (and isn't nil), the comparison is between obj1 and
|
|
46
|
+
# the working tree; the patch reflects all changes since obj1.
|
|
47
|
+
#
|
|
48
|
+
# **Comparing the index against the working tree**
|
|
49
|
+
#
|
|
50
|
+
# When obj1 is explicitly `nil` then obj2 must be omitted or `nil`. In this case,
|
|
51
|
+
# the comparison is between the index and the working tree; the patch reflects
|
|
52
|
+
# unstaged changes.
|
|
53
|
+
#
|
|
54
|
+
# @example Get the working tree patch since HEAD
|
|
55
|
+
# repo.diff_full #=> "diff --git a/lib/foo.rb b/lib/foo.rb\n..."
|
|
56
|
+
#
|
|
57
|
+
# @example Compare two specific commits
|
|
58
|
+
# repo.diff_full('abc1234', 'def5678')
|
|
59
|
+
#
|
|
60
|
+
# @example Get unstaged changes (index vs. working tree)
|
|
61
|
+
# repo.diff_full(nil)
|
|
62
|
+
#
|
|
63
|
+
# @example Limit the diff to a sub-path
|
|
64
|
+
# repo.diff_full('HEAD~1', 'HEAD', path_limiter: 'lib/')
|
|
65
|
+
#
|
|
66
|
+
# @param obj1 [String, nil] the first commit or object to compare; defaults to
|
|
67
|
+
# `'HEAD'`
|
|
68
|
+
#
|
|
69
|
+
# @param obj2 [String, nil] the second commit or object to compare
|
|
70
|
+
#
|
|
71
|
+
# @param opts [Hash] options to filter the diff
|
|
72
|
+
#
|
|
73
|
+
# @option opts [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
|
|
74
|
+
# limit the diff to the given path(s)
|
|
75
|
+
#
|
|
76
|
+
# @return [String] the unified diff patch output
|
|
77
|
+
#
|
|
78
|
+
# @raise [ArgumentError] if unsupported options are provided
|
|
79
|
+
#
|
|
80
|
+
# @raise [ArgumentError] if `obj1` is `nil` but `obj2` is not OR if `obj1` or `obj2` starts with `"-"`
|
|
81
|
+
#
|
|
82
|
+
# @raise [Git::FailedError] if git exits outside the allowed range (exit code > 2)
|
|
83
|
+
#
|
|
84
|
+
# @see https://git-scm.com/docs/git-diff git-diff documentation
|
|
85
|
+
#
|
|
86
|
+
def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
87
|
+
SharedPrivate.assert_valid_opts!(DIFF_FULL_ALLOWED_OPTS, **opts)
|
|
88
|
+
raise ArgumentError, 'Invalid arguments: obj1 is nil but obj2 is not' if obj1.nil? && !obj2.nil?
|
|
89
|
+
|
|
90
|
+
pathspecs = Private.normalize_pathspecs(opts[:path_limiter], 'path limiter')
|
|
91
|
+
result = Git::Commands::Diff.new(@execution_context).call(
|
|
92
|
+
*[obj1, obj2].compact,
|
|
93
|
+
patch: true, numstat: true, shortstat: true,
|
|
94
|
+
src_prefix: 'a/', dst_prefix: 'b/',
|
|
95
|
+
path: pathspecs
|
|
96
|
+
)
|
|
97
|
+
Private.extract_patch_text(result.stdout)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Option keys accepted by {#diff_numstat}
|
|
101
|
+
#
|
|
102
|
+
# @return [Array<Symbol>]
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
105
|
+
#
|
|
106
|
+
DIFF_NUMSTAT_ALLOWED_OPTS = %i[path_limiter].freeze
|
|
107
|
+
private_constant :DIFF_NUMSTAT_ALLOWED_OPTS
|
|
108
|
+
|
|
109
|
+
# Returns per-file insertion/deletion counts and totals between two trees
|
|
110
|
+
#
|
|
111
|
+
# Compares (1) two commits, (2) a commit against the working tree, or (3) the
|
|
112
|
+
# index against the working tree using `git diff --numstat`, and returns a
|
|
113
|
+
# structured hash of per-file insertion and deletion line counts together with
|
|
114
|
+
# aggregate totals.
|
|
115
|
+
#
|
|
116
|
+
# **Comparing two commits**
|
|
117
|
+
#
|
|
118
|
+
# When both obj1 and obj2 are provided, the comparison is between those two
|
|
119
|
+
# refs (commits, tags, branches, etc.).
|
|
120
|
+
#
|
|
121
|
+
# **Comparing a commit against the working tree**
|
|
122
|
+
#
|
|
123
|
+
# When only obj1 is provided (and isn't nil), the comparison is between obj1 and
|
|
124
|
+
# the working tree; the stats reflect all changes since obj1.
|
|
125
|
+
#
|
|
126
|
+
# **Comparing the index against the working tree**
|
|
127
|
+
#
|
|
128
|
+
# When obj1 is explicitly `nil` then obj2 must be omitted or `nil`. In this case,
|
|
129
|
+
# the comparison is between the index and the working tree; the stats reflect
|
|
130
|
+
# unstaged changes.
|
|
131
|
+
#
|
|
132
|
+
# @example Compare two specific commits
|
|
133
|
+
# repo.diff_numstat('abc1234', 'def5678')
|
|
134
|
+
#
|
|
135
|
+
# @example Get working tree changes since HEAD
|
|
136
|
+
# repo.diff_numstat #=> {
|
|
137
|
+
# total: { insertions: 5, deletions: 2, lines: 7, files: 1 },
|
|
138
|
+
# files: { "lib/foo.rb" => { insertions: 5, deletions: 2 } }
|
|
139
|
+
# }
|
|
140
|
+
#
|
|
141
|
+
# @example Get unstaged changes (index vs. working tree)
|
|
142
|
+
# repo.diff_numstat(nil) #=> { ... }
|
|
143
|
+
#
|
|
144
|
+
# @example Limit the stats to a sub-path
|
|
145
|
+
# repo.diff_numstat('HEAD~1', 'HEAD', path_limiter: 'lib/')
|
|
146
|
+
#
|
|
147
|
+
# @param obj1 [String, nil] the first commit or object to compare; defaults to
|
|
148
|
+
# `'HEAD'`
|
|
149
|
+
#
|
|
150
|
+
# @param obj2 [String, nil] the second commit or object to compare
|
|
151
|
+
#
|
|
152
|
+
# @param opts [Hash] options to filter the diff
|
|
153
|
+
#
|
|
154
|
+
# @option opts [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
|
|
155
|
+
# limit the stats to the given path(s)
|
|
156
|
+
#
|
|
157
|
+
# @return [Hash] per-file insertion and deletion counts plus aggregate totals
|
|
158
|
+
#
|
|
159
|
+
# ```
|
|
160
|
+
# {
|
|
161
|
+
# total: { insertions: Integer, deletions: Integer, lines: Integer, files: Integer },
|
|
162
|
+
# files: { "path/to/file" => { insertions: Integer, deletions: Integer } }
|
|
163
|
+
# }
|
|
164
|
+
# ```
|
|
165
|
+
#
|
|
166
|
+
# @raise [ArgumentError] if unsupported options are provided
|
|
167
|
+
#
|
|
168
|
+
# @raise [ArgumentError] if `obj1` is `nil` but `obj2` is not OR if `obj1` or `obj2` starts with `"-"`
|
|
169
|
+
#
|
|
170
|
+
# @raise [Git::FailedError] if git exits outside the allowed range (exit code > 2)
|
|
171
|
+
#
|
|
172
|
+
# @see https://git-scm.com/docs/git-diff git-diff documentation
|
|
173
|
+
#
|
|
174
|
+
def diff_numstat(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
175
|
+
SharedPrivate.assert_valid_opts!(DIFF_NUMSTAT_ALLOWED_OPTS, **opts)
|
|
176
|
+
raise ArgumentError, 'Invalid arguments: obj1 is nil but obj2 is not' if obj1.nil? && !obj2.nil?
|
|
177
|
+
|
|
178
|
+
pathspecs = Private.normalize_pathspecs(opts[:path_limiter], 'path limiter')
|
|
179
|
+
result = Git::Commands::Diff.new(@execution_context).call(
|
|
180
|
+
*[obj1, obj2].compact,
|
|
181
|
+
numstat: true, shortstat: true, src_prefix: 'a/', dst_prefix: 'b/',
|
|
182
|
+
path: pathspecs
|
|
183
|
+
)
|
|
184
|
+
Private.parse_numstat_output(result.stdout)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Option keys accepted by {#diff_stats}
|
|
188
|
+
#
|
|
189
|
+
# @return [Array<Symbol>]
|
|
190
|
+
#
|
|
191
|
+
# @api private
|
|
192
|
+
#
|
|
193
|
+
DIFF_STATS_ALLOWED_OPTS = %i[path_limiter].freeze
|
|
194
|
+
private_constant :DIFF_STATS_ALLOWED_OPTS
|
|
195
|
+
|
|
196
|
+
# Returns the stats between two trees as a {Git::DiffStats} object
|
|
197
|
+
#
|
|
198
|
+
# Compares (1) two commits, (2) a commit against the working tree, or (3) the
|
|
199
|
+
# index against the working tree and constructs a lazy {Git::DiffStats} that
|
|
200
|
+
# computes per-file insertion and deletion counts on demand when its accessor
|
|
201
|
+
# methods are called.
|
|
202
|
+
#
|
|
203
|
+
# **Comparing two commits**
|
|
204
|
+
#
|
|
205
|
+
# When both obj1 and obj2 are provided, the comparison is between those two
|
|
206
|
+
# refs (commits, tags, branches, etc.).
|
|
207
|
+
#
|
|
208
|
+
# **Comparing a commit against the working tree**
|
|
209
|
+
#
|
|
210
|
+
# When only obj1 is provided (and isn't nil), the comparison is between obj1 and
|
|
211
|
+
# the working tree; the stats reflect all changes since obj1.
|
|
212
|
+
#
|
|
213
|
+
# **Comparing the index against the working tree**
|
|
214
|
+
#
|
|
215
|
+
# When obj1 is explicitly `nil` then obj2 must be omitted or `nil`. In this case,
|
|
216
|
+
# the comparison is between the index and the working tree; the stats reflect
|
|
217
|
+
# unstaged changes.
|
|
218
|
+
#
|
|
219
|
+
# @example Get working tree stats since HEAD
|
|
220
|
+
# stats = repo.diff_stats
|
|
221
|
+
# stats.insertions #=> 3
|
|
222
|
+
# stats.deletions #=> 1
|
|
223
|
+
#
|
|
224
|
+
# @example Compare two specific commits
|
|
225
|
+
# repo.diff_stats('abc1234', 'def5678')
|
|
226
|
+
#
|
|
227
|
+
# @example Get unstaged stats (index vs. working tree)
|
|
228
|
+
# repo.diff_stats(nil).insertions
|
|
229
|
+
#
|
|
230
|
+
# @example Limit stats to a sub-path
|
|
231
|
+
# repo.diff_stats('HEAD~1', 'HEAD', path_limiter: 'lib/')
|
|
232
|
+
#
|
|
233
|
+
# @param obj1 [String, nil] the first commit or object to compare; defaults to
|
|
234
|
+
# `'HEAD'`
|
|
235
|
+
#
|
|
236
|
+
# @param obj2 [String, nil] the second commit or object to compare
|
|
237
|
+
#
|
|
238
|
+
# @param opts [Hash] options to filter the diff
|
|
239
|
+
#
|
|
240
|
+
# @option opts [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
|
|
241
|
+
# limit the stats to the given path(s)
|
|
242
|
+
#
|
|
243
|
+
# @return [Git::DiffStats] a lazy stats object for the comparison
|
|
244
|
+
#
|
|
245
|
+
# @raise [ArgumentError] if unsupported options are provided
|
|
246
|
+
#
|
|
247
|
+
# @raise [ArgumentError] if `obj1` is `nil` but `obj2` is not OR if `obj1` or `obj2` starts with `"-"`
|
|
248
|
+
#
|
|
249
|
+
# @see #diff_numstat
|
|
250
|
+
#
|
|
251
|
+
# @see https://git-scm.com/docs/git-diff git-diff documentation
|
|
252
|
+
#
|
|
253
|
+
def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
254
|
+
SharedPrivate.assert_valid_opts!(DIFF_STATS_ALLOWED_OPTS, **opts)
|
|
255
|
+
raise ArgumentError, 'Invalid arguments: obj1 is nil but obj2 is not' if obj1.nil? && !obj2.nil?
|
|
256
|
+
|
|
257
|
+
Git::DiffStats.new(self, obj1, obj2, opts[:path_limiter])
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Returns a lazy {Git::Diff} object for the comparison between two trees
|
|
261
|
+
#
|
|
262
|
+
# Compares (1) two commits, (2) a commit against the working tree, or (3) the
|
|
263
|
+
# index against the working tree. The returned {Git::Diff} is lazy — it does
|
|
264
|
+
# not run any git commands until an accessor method (e.g., {Git::Diff#patch},
|
|
265
|
+
# {Git::Diff#each}) is called.
|
|
266
|
+
#
|
|
267
|
+
# Use {Git::Diff#path} to limit the diff to a sub-path after construction.
|
|
268
|
+
#
|
|
269
|
+
# @example Get the diff since HEAD
|
|
270
|
+
# diff = repo.diff
|
|
271
|
+
# diff.patch #=> "diff --git a/lib/foo.rb ..."
|
|
272
|
+
#
|
|
273
|
+
# @example Compare two specific commits
|
|
274
|
+
# repo.diff('abc1234', 'def5678').patch
|
|
275
|
+
#
|
|
276
|
+
# @example Limit to a sub-path
|
|
277
|
+
# repo.diff('HEAD~1', 'HEAD').path('lib/').patch
|
|
278
|
+
#
|
|
279
|
+
# @example Get unstaged changes (index vs. working tree)
|
|
280
|
+
# repo.diff(nil).patch
|
|
281
|
+
#
|
|
282
|
+
# @param obj1 [String, nil] the first commit or object to compare; defaults to
|
|
283
|
+
# `'HEAD'`
|
|
284
|
+
#
|
|
285
|
+
# @param obj2 [String, nil] the second commit or object to compare
|
|
286
|
+
#
|
|
287
|
+
# @return [Git::Diff] a lazy diff object for the comparison
|
|
288
|
+
#
|
|
289
|
+
# @see https://git-scm.com/docs/git-diff git-diff documentation
|
|
290
|
+
#
|
|
291
|
+
def diff(obj1 = 'HEAD', obj2 = nil)
|
|
292
|
+
Git::Diff.new(self, obj1, obj2)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Option keys accepted by {#diff_path_status}
|
|
296
|
+
#
|
|
297
|
+
# @return [Array<Symbol>]
|
|
298
|
+
#
|
|
299
|
+
# @api private
|
|
300
|
+
#
|
|
301
|
+
DIFF_PATH_STATUS_ALLOWED_OPTS = %i[path_limiter path].freeze
|
|
302
|
+
private_constant :DIFF_PATH_STATUS_ALLOWED_OPTS
|
|
303
|
+
|
|
304
|
+
# Returns the file path status between two trees
|
|
305
|
+
#
|
|
306
|
+
# Compares (1) two commits, (2) a commit against the working tree, or (3) the
|
|
307
|
+
# index against the working tree and returns a {Git::DiffPathStatus} enumerating
|
|
308
|
+
# each changed file together with its status code (e.g. `"M"` for modified,
|
|
309
|
+
# `"A"` for added, `"D"` for deleted, `"R100"` for a rename with 100%
|
|
310
|
+
# similarity, etc.).
|
|
311
|
+
#
|
|
312
|
+
# **Comparing two commits**
|
|
313
|
+
#
|
|
314
|
+
# When both from and to are provided, the comparison is between those two
|
|
315
|
+
# refs (commits, tags, branches, etc.).
|
|
316
|
+
#
|
|
317
|
+
# **Comparing a commit against the working tree**
|
|
318
|
+
#
|
|
319
|
+
# When only from is provided (and isn't nil), the comparison is between from and
|
|
320
|
+
# the working tree; the status reflects all changes since from.
|
|
321
|
+
#
|
|
322
|
+
# **Comparing the index against the working tree**
|
|
323
|
+
#
|
|
324
|
+
# When from is explicitly `nil` then to must be omitted or `nil`. In this case,
|
|
325
|
+
# the comparison is between the index and the working tree; the status reflects
|
|
326
|
+
# unstaged changes.
|
|
327
|
+
#
|
|
328
|
+
# @example Get working tree path changes since HEAD
|
|
329
|
+
# repo.diff_path_status #=> #<Git::DiffPathStatus ...>
|
|
330
|
+
# repo.diff_path_status.to_h #=> { "README.md" => "M", "lib/foo.rb" => "A" }
|
|
331
|
+
#
|
|
332
|
+
# @example Compare two specific commits
|
|
333
|
+
# repo.diff_path_status('abc1234', 'def5678').to_h
|
|
334
|
+
#
|
|
335
|
+
# @example Get unstaged path changes (index vs. working tree)
|
|
336
|
+
# repo.diff_path_status(nil).to_h
|
|
337
|
+
#
|
|
338
|
+
# @example Limit the comparison to a sub-path
|
|
339
|
+
# repo.diff_path_status('HEAD~1', 'HEAD', path_limiter: 'lib/')
|
|
340
|
+
#
|
|
341
|
+
# @param from [String, nil] the first commit or object to compare; defaults to
|
|
342
|
+
# `'HEAD'`
|
|
343
|
+
#
|
|
344
|
+
# @param to [String, nil] the second commit or object to compare
|
|
345
|
+
#
|
|
346
|
+
# @param opts [Hash] options to filter the diff
|
|
347
|
+
#
|
|
348
|
+
# @option opts [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
|
|
349
|
+
# limit the status report to the given path(s)
|
|
350
|
+
#
|
|
351
|
+
# @option opts [String, Pathname, Array<String, Pathname>, nil] :path (nil)
|
|
352
|
+
# **deprecated** — use `:path_limiter` instead
|
|
353
|
+
#
|
|
354
|
+
# @return [Git::DiffPathStatus] the name-status report for the comparison
|
|
355
|
+
#
|
|
356
|
+
# @raise [ArgumentError] if unsupported options are provided
|
|
357
|
+
#
|
|
358
|
+
# @raise [ArgumentError] if `from` is `nil` but `to` is not OR if `from` or `to` starts with `"-"`
|
|
359
|
+
#
|
|
360
|
+
# @raise [Git::FailedError] if git exits outside the allowed range (exit code > 2)
|
|
361
|
+
#
|
|
362
|
+
# @see https://git-scm.com/docs/git-diff git-diff documentation
|
|
363
|
+
#
|
|
364
|
+
def diff_path_status(from = 'HEAD', to = nil, opts = {})
|
|
365
|
+
SharedPrivate.assert_valid_opts!(DIFF_PATH_STATUS_ALLOWED_OPTS, **opts)
|
|
366
|
+
raise ArgumentError, 'Invalid arguments: `from` is nil but `to` is not' if from.nil? && !to.nil?
|
|
367
|
+
|
|
368
|
+
path_limiter = Private.resolve_path_limiter(opts)
|
|
369
|
+
pathspecs = Private.normalize_pathspecs(path_limiter, 'path limiter')
|
|
370
|
+
|
|
371
|
+
result = Private.call_diff_command(@execution_context, from, to, pathspecs)
|
|
372
|
+
Git::DiffPathStatus.new(Private.extract_name_status_from_raw(result.stdout))
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Alias for {#diff_path_status}; provided for backward compatibility
|
|
376
|
+
#
|
|
377
|
+
# @return [Git::DiffPathStatus] the name-status report for the comparison
|
|
378
|
+
#
|
|
379
|
+
# @deprecated Use {#diff_path_status} instead
|
|
380
|
+
#
|
|
381
|
+
# @see #diff_path_status
|
|
382
|
+
alias diff_name_status diff_path_status
|
|
383
|
+
|
|
384
|
+
# Compares the index and the working directory
|
|
385
|
+
#
|
|
386
|
+
# Runs `git diff-files` to list files that differ between the index
|
|
387
|
+
# (staging area) and the working directory. These are changes that have
|
|
388
|
+
# been made to tracked files but not yet staged.
|
|
389
|
+
#
|
|
390
|
+
# @note The field names in the returned hash are **legacy names** inherited
|
|
391
|
+
# from `Git::Lib#diff_files` and appear counterintuitive: `:mode_repo`
|
|
392
|
+
# and `:sha_repo` hold **index (staging area)** values, while
|
|
393
|
+
# `:mode_index` and `:sha_index` hold **working tree** values.
|
|
394
|
+
#
|
|
395
|
+
# @example List all files with unstaged changes
|
|
396
|
+
# repo.diff_files
|
|
397
|
+
# #=> {
|
|
398
|
+
# # "lib/foo.rb" => {
|
|
399
|
+
# # mode_index: "100644", mode_repo: "100644",
|
|
400
|
+
# # path: "lib/foo.rb", sha_repo: "abc1234",
|
|
401
|
+
# # sha_index: "0000000000000000000000000000000000000000",
|
|
402
|
+
# # type: "M"
|
|
403
|
+
# # }
|
|
404
|
+
# # }
|
|
405
|
+
#
|
|
406
|
+
# @return [Hash{String => Hash}] a hash keyed by file path
|
|
407
|
+
#
|
|
408
|
+
# Each value is a hash with the following keys (note the legacy naming
|
|
409
|
+
# where `:*_repo` holds index data and `:*_index` holds working tree data):
|
|
410
|
+
#
|
|
411
|
+
# * `:mode_index` [String] the working tree file mode (legacy name)
|
|
412
|
+
# * `:mode_repo` [String] the index (staging area) file mode (legacy name)
|
|
413
|
+
# * `:path` [String] the file path
|
|
414
|
+
# * `:sha_repo` [String] the SHA of the object in the index (staging area) (legacy name)
|
|
415
|
+
# * `:sha_index` [String] the SHA of the object in the working tree; all
|
|
416
|
+
# zeros when git has not computed the working tree blob SHA (legacy name)
|
|
417
|
+
# * `:type` [String] the status code (e.g. `"M"`, `"A"`, `"D"`)
|
|
418
|
+
#
|
|
419
|
+
# @raise [Git::FailedError] if git exits outside the allowed range (exit code > 1)
|
|
420
|
+
#
|
|
421
|
+
# @see https://git-scm.com/docs/git-diff-files git-diff-files documentation
|
|
422
|
+
#
|
|
423
|
+
def diff_files
|
|
424
|
+
Git::Commands::Status.new(@execution_context).call
|
|
425
|
+
Private.parse_diff_files_output(
|
|
426
|
+
Git::Commands::DiffFiles.new(@execution_context).call.stdout
|
|
427
|
+
)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Compares the working tree against the given tree object
|
|
431
|
+
#
|
|
432
|
+
# Runs `git diff-index <treeish>` (without `--cached`) to list files that
|
|
433
|
+
# differ between the given tree object (e.g. a commit or `"HEAD"`) and the
|
|
434
|
+
# working tree. The index is refreshed via `git status` first so that cached
|
|
435
|
+
# stat information is up to date.
|
|
436
|
+
#
|
|
437
|
+
# This is equivalent to the 4.x `Git::Lib#diff_index` behavior, which also
|
|
438
|
+
# ran `git diff-index` without `--cached`.
|
|
439
|
+
#
|
|
440
|
+
# @note `git diff-index` without `--cached` uses the index as a stat cache:
|
|
441
|
+
# any file whose index entry differs from the tree is reported as changed,
|
|
442
|
+
# even when the on-disk working-tree content is byte-for-byte identical to
|
|
443
|
+
# the tree. A staged change that has been reverted in the working tree will
|
|
444
|
+
# therefore still appear in the result (because the index still differs from
|
|
445
|
+
# the tree).
|
|
446
|
+
#
|
|
447
|
+
# @note The field names in the returned hash are **legacy names** inherited
|
|
448
|
+
# from `Git::Lib#diff_index` and appear counterintuitive: `:mode_repo`
|
|
449
|
+
# and `:sha_repo` hold **tree (treeish)** values, while `:mode_index` and
|
|
450
|
+
# `:sha_index` hold **working tree** values.
|
|
451
|
+
#
|
|
452
|
+
# @example List all working-tree files that differ from HEAD
|
|
453
|
+
# repo.diff_index('HEAD')
|
|
454
|
+
# #=> {
|
|
455
|
+
# # "lib/foo.rb" => {
|
|
456
|
+
# # mode_index: "100644", mode_repo: "100644",
|
|
457
|
+
# # path: "lib/foo.rb", sha_repo: "abc1234",
|
|
458
|
+
# # sha_index: "0000000000000000000000000000000000000000",
|
|
459
|
+
# # type: "M"
|
|
460
|
+
# # }
|
|
461
|
+
# # }
|
|
462
|
+
#
|
|
463
|
+
# @param treeish [String] the tree object to compare against (e.g. `'HEAD'`,
|
|
464
|
+
# a commit SHA, or a tag name)
|
|
465
|
+
#
|
|
466
|
+
# @return [Hash{String => Hash}] a hash keyed by file path
|
|
467
|
+
#
|
|
468
|
+
# Each value is a hash with the following keys (note the legacy naming
|
|
469
|
+
# where `:*_repo` holds tree data and `:*_index` holds working tree data):
|
|
470
|
+
#
|
|
471
|
+
# * `:mode_index` [String] the working tree file mode (legacy name)
|
|
472
|
+
# * `:mode_repo` [String] the tree (treeish) file mode (legacy name)
|
|
473
|
+
# * `:path` [String] the file path
|
|
474
|
+
# * `:sha_repo` [String] the SHA of the object in the tree (treeish) (legacy name)
|
|
475
|
+
# * `:sha_index` [String] the SHA of the object in the working tree; all
|
|
476
|
+
# zeros when git has not yet computed the working tree blob SHA (legacy name)
|
|
477
|
+
# * `:type` [String] the status code (e.g. `"M"`, `"A"`, `"D"`)
|
|
478
|
+
#
|
|
479
|
+
# @raise [Git::FailedError] if git exits outside the allowed range (exit code > 1)
|
|
480
|
+
#
|
|
481
|
+
# @see https://git-scm.com/docs/git-diff-index git-diff-index documentation
|
|
482
|
+
#
|
|
483
|
+
def diff_index(treeish)
|
|
484
|
+
Git::Commands::Status.new(@execution_context).call
|
|
485
|
+
Private.parse_diff_files_output(
|
|
486
|
+
Git::Commands::DiffIndex.new(@execution_context).call(treeish).stdout
|
|
487
|
+
)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Private helpers local to {Git::Repository::Diffing}
|
|
491
|
+
#
|
|
492
|
+
# @api private
|
|
493
|
+
#
|
|
494
|
+
module Private
|
|
495
|
+
module_function
|
|
496
|
+
|
|
497
|
+
# Resolves the effective path limiter from the options hash
|
|
498
|
+
#
|
|
499
|
+
# When `:path_limiter` is present it is used directly and no warning is
|
|
500
|
+
# emitted. When only `:path` is present a deprecation warning is emitted
|
|
501
|
+
# and its value is used. Returns `nil` when neither key is present.
|
|
502
|
+
#
|
|
503
|
+
# @param opts [Hash] the options hash from {#diff_path_status}
|
|
504
|
+
#
|
|
505
|
+
# @return [String, Pathname, Array<String, Pathname>, nil]
|
|
506
|
+
# the effective path limiter
|
|
507
|
+
#
|
|
508
|
+
def resolve_path_limiter(opts)
|
|
509
|
+
if opts.key?(:path_limiter)
|
|
510
|
+
opts[:path_limiter]
|
|
511
|
+
elsif opts.key?(:path)
|
|
512
|
+
Git::Deprecation.warn(
|
|
513
|
+
'Git::Repository#diff_path_status :path option is deprecated. Use :path_limiter instead.'
|
|
514
|
+
)
|
|
515
|
+
opts[:path]
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Extracts only the patch text from combined diff command output
|
|
520
|
+
#
|
|
521
|
+
# When {Git::Commands::Diff} is called with `patch: true, numstat: true,
|
|
522
|
+
# shortstat: true`, the stdout contains numstat lines, a shortstat summary
|
|
523
|
+
# line, and then the unified patch text starting at `"diff --git "`. This
|
|
524
|
+
# method strips the leading numstat/shortstat lines and returns only the
|
|
525
|
+
# patch portion.
|
|
526
|
+
#
|
|
527
|
+
# @param output [String] combined command output
|
|
528
|
+
#
|
|
529
|
+
# @return [String] only the patch text (may be empty when there are no
|
|
530
|
+
# changes)
|
|
531
|
+
#
|
|
532
|
+
def extract_patch_text(output)
|
|
533
|
+
match = output.match(/^diff --git /m)
|
|
534
|
+
match ? output[match.begin(0)..] : output
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Runs git-diff with `--raw` format options and returns the result
|
|
538
|
+
#
|
|
539
|
+
# @param execution_context [Git::ExecutionContext] the execution context
|
|
540
|
+
# used to run git commands
|
|
541
|
+
#
|
|
542
|
+
# @param from [String] first ref
|
|
543
|
+
#
|
|
544
|
+
# @param to [String, nil] second ref
|
|
545
|
+
#
|
|
546
|
+
# @param pathspecs [Array<String>, nil] path limiters
|
|
547
|
+
#
|
|
548
|
+
# @return [Git::CommandLineResult] the result of calling `git diff`
|
|
549
|
+
#
|
|
550
|
+
def call_diff_command(execution_context, from, to, pathspecs)
|
|
551
|
+
Git::Commands::Diff.new(execution_context).call(
|
|
552
|
+
*[from, to].compact,
|
|
553
|
+
raw: true, numstat: true, shortstat: true,
|
|
554
|
+
src_prefix: 'a/', dst_prefix: 'b/',
|
|
555
|
+
path: pathspecs
|
|
556
|
+
)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# Normalizes path specifications for Git commands
|
|
560
|
+
#
|
|
561
|
+
# @param pathspecs [String, Pathname, Array<String, Pathname>, nil]
|
|
562
|
+
# the path(s) to normalize
|
|
563
|
+
#
|
|
564
|
+
# @param arg_name [String] the argument name used in error messages
|
|
565
|
+
#
|
|
566
|
+
# @return [Array<String>, nil] the normalized paths, or `nil` if none are valid
|
|
567
|
+
#
|
|
568
|
+
# @raise [ArgumentError] if any path is not a `String` or `Pathname`
|
|
569
|
+
#
|
|
570
|
+
def normalize_pathspecs(pathspecs, arg_name)
|
|
571
|
+
return nil unless pathspecs
|
|
572
|
+
|
|
573
|
+
normalized = Array(pathspecs)
|
|
574
|
+
validate_pathspec_types(normalized, arg_name)
|
|
575
|
+
|
|
576
|
+
normalized = normalized.map(&:to_s).reject(&:empty?)
|
|
577
|
+
return nil if normalized.empty?
|
|
578
|
+
|
|
579
|
+
normalized
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Raises an error if any element of `pathspecs` is not a `String` or `Pathname`
|
|
583
|
+
#
|
|
584
|
+
# @param pathspecs [Array] the path elements to validate
|
|
585
|
+
#
|
|
586
|
+
# @param arg_name [String] the argument name used in error messages
|
|
587
|
+
#
|
|
588
|
+
# @return [void]
|
|
589
|
+
#
|
|
590
|
+
# @raise [ArgumentError] if any element is not a `String` or `Pathname`
|
|
591
|
+
#
|
|
592
|
+
def validate_pathspec_types(pathspecs, arg_name)
|
|
593
|
+
return if pathspecs.all? { |p| p.is_a?(String) || p.is_a?(Pathname) }
|
|
594
|
+
|
|
595
|
+
raise ArgumentError, "Invalid #{arg_name}: must be a String, Pathname, or Array of Strings/Pathnames"
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# Parses raw `git diff-files` output into a file-keyed hash
|
|
599
|
+
#
|
|
600
|
+
# Each output line has the format:
|
|
601
|
+
# `:old_mode new_mode old_sha new_sha status\tpath`
|
|
602
|
+
#
|
|
603
|
+
# The leading colon on `old_mode` is stripped when building
|
|
604
|
+
# the `:mode_repo` value.
|
|
605
|
+
#
|
|
606
|
+
# @param stdout [String] raw stdout from {Git::Commands::DiffFiles#call}
|
|
607
|
+
#
|
|
608
|
+
# @return [Hash{String => Hash}] a hash keyed by file path where each
|
|
609
|
+
# value has keys `:mode_index`, `:mode_repo`, `:path`, `:sha_repo`,
|
|
610
|
+
# `:sha_index`, and `:type`
|
|
611
|
+
#
|
|
612
|
+
def parse_diff_files_output(stdout)
|
|
613
|
+
stdout.split("\n").each_with_object({}) do |line, memo|
|
|
614
|
+
next if line.empty?
|
|
615
|
+
|
|
616
|
+
tab_pos = line.index("\t")
|
|
617
|
+
next unless tab_pos
|
|
618
|
+
|
|
619
|
+
path, entry = parse_diff_files_line(line, tab_pos)
|
|
620
|
+
memo[path] = entry
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Parses a single raw `git diff-files` output line into a path/entry pair
|
|
625
|
+
#
|
|
626
|
+
# @param line [String] a single non-empty line containing a tab character
|
|
627
|
+
# @param tab_pos [Integer] the index of the first tab in the line
|
|
628
|
+
#
|
|
629
|
+
# @return [Array(String, Hash)] two-element array of `[path, entry_hash]`
|
|
630
|
+
#
|
|
631
|
+
def parse_diff_files_line(line, tab_pos)
|
|
632
|
+
path = unescape_quoted_path(line[(tab_pos + 1)..])
|
|
633
|
+
parts = line[0, tab_pos].split
|
|
634
|
+
[path, build_diff_files_entry(path, parts)]
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Builds a single file-info hash for {#parse_diff_files_output}
|
|
638
|
+
#
|
|
639
|
+
# @param path [String] the file path
|
|
640
|
+
# @param parts [Array<String>] the whitespace-split fields from the info
|
|
641
|
+
# portion of the diff-files line: `[mode_src, mode_dest, sha_src,
|
|
642
|
+
# sha_dest, type]`
|
|
643
|
+
#
|
|
644
|
+
# @return [Hash] entry hash with keys `:mode_index`, `:mode_repo`, `:path`,
|
|
645
|
+
# `:sha_repo`, `:sha_index`, `:type`
|
|
646
|
+
#
|
|
647
|
+
def build_diff_files_entry(path, parts)
|
|
648
|
+
{
|
|
649
|
+
mode_index: parts[1],
|
|
650
|
+
mode_repo: parts[0].to_s[1, 7],
|
|
651
|
+
path: path,
|
|
652
|
+
sha_repo: parts[2],
|
|
653
|
+
sha_index: parts[3],
|
|
654
|
+
type: parts[4]
|
|
655
|
+
}
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# Extracts name-status data from `--raw` diff output lines
|
|
659
|
+
#
|
|
660
|
+
# Raw lines have the format:
|
|
661
|
+
# :old_mode new_mode old_sha new_sha status\tpath
|
|
662
|
+
# or for renames/copies:
|
|
663
|
+
# :old_mode new_mode old_sha new_sha Rxx\told_path\tnew_path
|
|
664
|
+
#
|
|
665
|
+
# @param output [String] raw diff output
|
|
666
|
+
#
|
|
667
|
+
# @return [Hash{String => String}] mapping of file paths to status tokens
|
|
668
|
+
#
|
|
669
|
+
def extract_name_status_from_raw(output)
|
|
670
|
+
output.split("\n").each_with_object({}) do |line, memo|
|
|
671
|
+
next unless line.start_with?(':')
|
|
672
|
+
|
|
673
|
+
parts = line[1..].split(/\s+/, 5)
|
|
674
|
+
status_and_paths = parts[4].split("\t")
|
|
675
|
+
status = status_and_paths[0]
|
|
676
|
+
path = status_and_paths.length > 2 ? status_and_paths[2] : status_and_paths[1]
|
|
677
|
+
memo[unescape_quoted_path(path)] = status
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Parses combined `--numstat --shortstat` output into an insertions/deletions hash
|
|
682
|
+
#
|
|
683
|
+
# Strips the trailing shortstat summary line and empty lines, parses the
|
|
684
|
+
# remaining numstat lines, and returns a structured hash with per-file
|
|
685
|
+
# stats and aggregated totals.
|
|
686
|
+
#
|
|
687
|
+
# @param output [String] raw stdout from `git diff --numstat --shortstat`
|
|
688
|
+
#
|
|
689
|
+
# @return [Hash] per-file insertion and deletion counts plus aggregate totals
|
|
690
|
+
#
|
|
691
|
+
# ```
|
|
692
|
+
# {
|
|
693
|
+
# total: { insertions: Integer, deletions: Integer, lines: Integer, files: Integer },
|
|
694
|
+
# files: { "path/to/file" => { insertions: Integer, deletions: Integer } }
|
|
695
|
+
# }
|
|
696
|
+
# ```
|
|
697
|
+
#
|
|
698
|
+
def parse_numstat_output(output)
|
|
699
|
+
file_stats = extract_numstat_lines(output).map { |line| parse_numstat_line(line) }
|
|
700
|
+
{ total: build_numstat_totals(file_stats), files: build_numstat_files(file_stats) }
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Builds the `:total` sub-hash for {#parse_numstat_output}
|
|
704
|
+
#
|
|
705
|
+
# @param file_stats [Array<Hash>] per-file stats from {#parse_numstat_line}
|
|
706
|
+
#
|
|
707
|
+
# @return [Hash] aggregate totals
|
|
708
|
+
#
|
|
709
|
+
# `{ insertions: Integer, deletions: Integer, lines: Integer, files: Integer }`
|
|
710
|
+
#
|
|
711
|
+
def build_numstat_totals(file_stats)
|
|
712
|
+
insertions = file_stats.sum { |s| s[:insertions] }
|
|
713
|
+
deletions = file_stats.sum { |s| s[:deletions] }
|
|
714
|
+
{ insertions: insertions, deletions: deletions,
|
|
715
|
+
lines: insertions + deletions, files: file_stats.size }
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# Builds the `:files` sub-hash for {#parse_numstat_output}
|
|
719
|
+
#
|
|
720
|
+
# @param file_stats [Array<Hash>] per-file stats from {#parse_numstat_line}
|
|
721
|
+
#
|
|
722
|
+
# @return [Hash{String => Hash}] per-file insertion and deletion counts
|
|
723
|
+
#
|
|
724
|
+
def build_numstat_files(file_stats)
|
|
725
|
+
file_stats.to_h { |s| [s[:filename], s.slice(:insertions, :deletions)] }
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Filters raw numstat+shortstat output to only the numstat lines
|
|
729
|
+
#
|
|
730
|
+
# @param output [String] combined command output
|
|
731
|
+
#
|
|
732
|
+
# @return [Array<String>] only the numstat lines (no empties, no shortstat line)
|
|
733
|
+
#
|
|
734
|
+
def extract_numstat_lines(output)
|
|
735
|
+
output.split("\n").reject { |l| l.empty? || l.match?(/^\s*\d+\s+files?\s+changed/) }
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# Parses a single `--numstat` line into a stats hash
|
|
739
|
+
#
|
|
740
|
+
# Numstat lines have the format `<insertions>\t<deletions>\t<path>`.
|
|
741
|
+
# Quoted paths (containing non-ASCII or special characters) are unescaped.
|
|
742
|
+
#
|
|
743
|
+
# @param line [String] a single numstat output line
|
|
744
|
+
#
|
|
745
|
+
# @return [Hash] `{ filename: String, insertions: Integer, deletions: Integer }`
|
|
746
|
+
#
|
|
747
|
+
def parse_numstat_line(line)
|
|
748
|
+
insertions_s, deletions_s, filename = line.split("\t", 3)
|
|
749
|
+
{ filename: unescape_quoted_path(filename), insertions: insertions_s.to_i, deletions: deletions_s.to_i }
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
# Unescapes a git-quoted path (e.g. `"quoted_file_\\342\\230\\240"`)
|
|
753
|
+
#
|
|
754
|
+
# Git quotes paths that contain non-ASCII or special characters by
|
|
755
|
+
# wrapping them in double-quotes and octal-escaping each byte. This
|
|
756
|
+
# method strips the surrounding quotes and delegates unescaping to
|
|
757
|
+
# {Git::EscapedPath}.
|
|
758
|
+
#
|
|
759
|
+
# @param path [String] the path as it appears in git output
|
|
760
|
+
#
|
|
761
|
+
# @return [String] the unescaped path
|
|
762
|
+
#
|
|
763
|
+
def unescape_quoted_path(path)
|
|
764
|
+
if path.start_with?('"') && path.end_with?('"')
|
|
765
|
+
Git::EscapedPath.new(path[1..-2]).unescape
|
|
766
|
+
else
|
|
767
|
+
path
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
private_constant :Private
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
end
|