git 1.19.1 → 5.5.0
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/.yardopts +5 -1
- data/AI_POLICY.md +24 -0
- data/CHANGELOG.md +689 -0
- data/CODE_OF_CONDUCT.md +25 -0
- data/CONTRIBUTING.md +1175 -97
- data/GOVERNANCE.md +106 -0
- data/LICENSE +1 -1
- data/MAINTAINERS.md +17 -4
- data/README.md +476 -320
- data/UPGRADING.md +1138 -0
- data/git.gemspec +124 -36
- data/lib/git/author.rb +39 -7
- data/lib/git/author_info.rb +66 -0
- data/lib/git/branch.rb +615 -65
- data/lib/git/branch_delete_failure.rb +34 -0
- data/lib/git/branch_delete_result.rb +66 -0
- data/lib/git/branch_info.rb +237 -0
- data/lib/git/branches.rb +167 -44
- data/lib/git/command_line/base.rb +247 -0
- data/lib/git/command_line/capturing.rb +308 -0
- data/lib/git/command_line/result.rb +88 -0
- data/lib/git/command_line/streaming.rb +236 -0
- data/lib/git/command_line.rb +52 -0
- data/lib/git/commands/add.rb +139 -0
- data/lib/git/commands/am/abort.rb +43 -0
- data/lib/git/commands/am/apply.rb +263 -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 +49 -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 +242 -0
- data/lib/git/commands/archive/list_formats.rb +46 -0
- data/lib/git/commands/archive.rb +145 -0
- data/lib/git/commands/arguments.rb +4521 -0
- data/lib/git/commands/base.rb +502 -0
- data/lib/git/commands/branch/copy.rb +102 -0
- data/lib/git/commands/branch/create.rb +177 -0
- data/lib/git/commands/branch/delete.rb +88 -0
- data/lib/git/commands/branch/list.rb +178 -0
- data/lib/git/commands/branch/move.rb +102 -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 +53 -0
- data/lib/git/commands/branch.rb +34 -0
- data/lib/git/commands/cat_file/batch.rb +385 -0
- data/lib/git/commands/cat_file/filtered.rb +105 -0
- data/lib/git/commands/cat_file/raw.rb +271 -0
- data/lib/git/commands/cat_file.rb +49 -0
- data/lib/git/commands/checkout/branch.rb +153 -0
- data/lib/git/commands/checkout/files.rb +116 -0
- data/lib/git/commands/checkout.rb +38 -0
- data/lib/git/commands/checkout_index.rb +106 -0
- data/lib/git/commands/clean.rb +102 -0
- data/lib/git/commands/clone.rb +241 -0
- data/lib/git/commands/commit.rb +273 -0
- data/lib/git/commands/commit_tree.rb +101 -0
- data/lib/git/commands/config_option_syntax/add.rb +86 -0
- data/lib/git/commands/config_option_syntax/get.rb +121 -0
- data/lib/git/commands/config_option_syntax/get_all.rb +118 -0
- data/lib/git/commands/config_option_syntax/get_color.rb +95 -0
- data/lib/git/commands/config_option_syntax/get_color_bool.rb +96 -0
- data/lib/git/commands/config_option_syntax/get_regexp.rb +119 -0
- data/lib/git/commands/config_option_syntax/get_urlmatch.rb +111 -0
- data/lib/git/commands/config_option_syntax/list.rb +111 -0
- data/lib/git/commands/config_option_syntax/remove_section.rb +79 -0
- data/lib/git/commands/config_option_syntax/rename_section.rb +83 -0
- data/lib/git/commands/config_option_syntax/replace_all.rb +109 -0
- data/lib/git/commands/config_option_syntax/set.rb +119 -0
- data/lib/git/commands/config_option_syntax/unset.rb +92 -0
- data/lib/git/commands/config_option_syntax/unset_all.rb +94 -0
- data/lib/git/commands/config_option_syntax.rb +56 -0
- data/lib/git/commands/describe.rb +156 -0
- data/lib/git/commands/diff.rb +657 -0
- data/lib/git/commands/diff_files.rb +519 -0
- data/lib/git/commands/diff_index.rb +499 -0
- data/lib/git/commands/fetch.rb +354 -0
- data/lib/git/commands/fsck.rb +138 -0
- data/lib/git/commands/gc.rb +134 -0
- data/lib/git/commands/grep.rb +339 -0
- data/lib/git/commands/init.rb +101 -0
- data/lib/git/commands/log.rb +634 -0
- data/lib/git/commands/ls_files.rb +195 -0
- data/lib/git/commands/ls_remote.rb +161 -0
- data/lib/git/commands/ls_tree.rb +135 -0
- data/lib/git/commands/maintenance/register.rb +77 -0
- data/lib/git/commands/maintenance/run.rb +109 -0
- data/lib/git/commands/maintenance/start.rb +71 -0
- data/lib/git/commands/maintenance/stop.rb +60 -0
- data/lib/git/commands/maintenance/unregister.rb +84 -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 +250 -0
- data/lib/git/commands/merge.rb +28 -0
- data/lib/git/commands/merge_base.rb +91 -0
- data/lib/git/commands/mv.rb +82 -0
- data/lib/git/commands/name_rev.rb +119 -0
- data/lib/git/commands/pull.rb +382 -0
- data/lib/git/commands/push.rb +251 -0
- data/lib/git/commands/read_tree.rb +154 -0
- data/lib/git/commands/remote/add.rb +96 -0
- data/lib/git/commands/remote/get_url.rb +68 -0
- data/lib/git/commands/remote/list.rb +56 -0
- data/lib/git/commands/remote/prune.rb +63 -0
- data/lib/git/commands/remote/remove.rb +52 -0
- data/lib/git/commands/remote/rename.rb +76 -0
- data/lib/git/commands/remote/set_branches.rb +70 -0
- data/lib/git/commands/remote/set_head.rb +89 -0
- data/lib/git/commands/remote/set_url.rb +78 -0
- data/lib/git/commands/remote/set_url_add.rb +70 -0
- data/lib/git/commands/remote/set_url_delete.rb +71 -0
- data/lib/git/commands/remote/show.rb +77 -0
- data/lib/git/commands/remote/update.rb +79 -0
- data/lib/git/commands/remote.rb +42 -0
- data/lib/git/commands/repack.rb +281 -0
- data/lib/git/commands/reset.rb +154 -0
- data/lib/git/commands/rev_parse.rb +304 -0
- data/lib/git/commands/revert/abort.rb +45 -0
- data/lib/git/commands/revert/continue.rb +62 -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 +158 -0
- data/lib/git/commands/revert.rb +29 -0
- data/lib/git/commands/rm.rb +113 -0
- data/lib/git/commands/show.rb +632 -0
- data/lib/git/commands/show_ref/exclude_existing.rb +119 -0
- data/lib/git/commands/show_ref/exists.rb +80 -0
- data/lib/git/commands/show_ref/list.rb +149 -0
- data/lib/git/commands/show_ref/verify.rb +122 -0
- data/lib/git/commands/show_ref.rb +42 -0
- data/lib/git/commands/stash/apply.rb +81 -0
- data/lib/git/commands/stash/branch.rb +67 -0
- data/lib/git/commands/stash/clear.rb +43 -0
- data/lib/git/commands/stash/create.rb +60 -0
- data/lib/git/commands/stash/drop.rb +73 -0
- data/lib/git/commands/stash/list.rb +43 -0
- data/lib/git/commands/stash/pop.rb +87 -0
- data/lib/git/commands/stash/push.rb +112 -0
- data/lib/git/commands/stash/show.rb +158 -0
- data/lib/git/commands/stash/store.rb +72 -0
- data/lib/git/commands/stash.rb +38 -0
- data/lib/git/commands/status.rb +174 -0
- data/lib/git/commands/symbolic_ref/delete.rb +72 -0
- data/lib/git/commands/symbolic_ref/read.rb +99 -0
- data/lib/git/commands/symbolic_ref/update.rb +79 -0
- data/lib/git/commands/symbolic_ref.rb +38 -0
- data/lib/git/commands/tag/create.rb +142 -0
- data/lib/git/commands/tag/delete.rb +57 -0
- data/lib/git/commands/tag/list.rb +146 -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 +145 -0
- data/lib/git/commands/update_ref/delete.rb +90 -0
- data/lib/git/commands/update_ref/update.rb +103 -0
- data/lib/git/commands/update_ref.rb +42 -0
- data/lib/git/commands/version.rb +60 -0
- data/lib/git/commands/worktree/add.rb +139 -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 +88 -0
- data/lib/git/config.rb +72 -5
- data/lib/git/config_entry_info.rb +106 -0
- data/lib/git/configuring.rb +795 -0
- data/lib/git/detached_head_info.rb +57 -0
- data/lib/git/diff.rb +437 -86
- data/lib/git/diff_file_numstat_info.rb +31 -0
- data/lib/git/diff_file_patch_info.rb +136 -0
- data/lib/git/diff_file_raw_info.rb +129 -0
- data/lib/git/diff_info.rb +162 -0
- data/lib/git/diff_path_status.rb +107 -0
- data/lib/git/diff_result.rb +34 -0
- data/lib/git/diff_stats.rb +111 -0
- data/lib/git/dirstat_info.rb +102 -0
- data/lib/git/encoding_utils.rb +32 -1
- data/lib/git/errors.rb +285 -0
- data/lib/git/escaped_path.rb +57 -5
- data/lib/git/execution_context/global.rb +31 -0
- data/lib/git/execution_context/repository.rb +151 -0
- data/lib/git/execution_context.rb +559 -0
- data/lib/git/factories.rb +813 -0
- data/lib/git/file_ref.rb +77 -0
- data/lib/git/fsck_object.rb +56 -0
- data/lib/git/fsck_result.rb +132 -0
- data/lib/git/log.rb +306 -90
- data/lib/git/object.rb +563 -141
- data/lib/git/parsers/branch.rb +240 -0
- data/lib/git/parsers/cat_file.rb +111 -0
- data/lib/git/parsers/config_entry.rb +110 -0
- data/lib/git/parsers/diff.rb +792 -0
- data/lib/git/parsers/fsck.rb +144 -0
- data/lib/git/parsers/grep.rb +42 -0
- data/lib/git/parsers/ls_remote.rb +79 -0
- data/lib/git/parsers/ls_tree.rb +58 -0
- data/lib/git/parsers/remote.rb +162 -0
- data/lib/git/parsers/stash.rb +292 -0
- data/lib/git/parsers/status.rb +251 -0
- data/lib/git/parsers/tag.rb +341 -0
- data/lib/git/parsers/worktree.rb +185 -0
- data/lib/git/path_resolver.rb +206 -0
- data/lib/git/remote.rb +165 -12
- data/lib/git/remote_info.rb +203 -0
- data/lib/git/repository/branching.rb +964 -0
- data/lib/git/repository/committing.rb +246 -0
- data/lib/git/repository/context_helpers.rb +293 -0
- data/lib/git/repository/diffing.rb +785 -0
- data/lib/git/repository/inspecting.rb +252 -0
- data/lib/git/repository/logging.rb +410 -0
- data/lib/git/repository/maintenance.rb +65 -0
- data/lib/git/repository/merging.rb +451 -0
- data/lib/git/repository/object_operations.rb +1551 -0
- data/lib/git/repository/remote_operations.rb +984 -0
- data/lib/git/repository/shared_private.rb +120 -0
- data/lib/git/repository/staging.rb +587 -0
- data/lib/git/repository/stashing.rb +623 -0
- data/lib/git/repository/status_operations.rb +249 -0
- data/lib/git/repository/worktree_operations.rb +339 -0
- data/lib/git/repository.rb +484 -2
- data/lib/git/stash.rb +109 -12
- data/lib/git/stash_info.rb +102 -0
- data/lib/git/stashes.rb +169 -26
- data/lib/git/status.rb +308 -122
- data/lib/git/status_file_info.rb +258 -0
- data/lib/git/status_info.rb +189 -0
- data/lib/git/tag_delete_failure.rb +34 -0
- data/lib/git/tag_delete_result.rb +66 -0
- data/lib/git/tag_info.rb +99 -0
- data/lib/git/url.rb +15 -8
- data/lib/git/version.rb +113 -2
- data/lib/git/version_constraint.rb +85 -0
- data/lib/git/worktree.rb +150 -8
- data/lib/git/worktree_info.rb +128 -0
- data/lib/git/worktrees.rb +118 -13
- data/lib/git.rb +632 -234
- metadata +369 -54
- data/.github/stale.yml +0 -25
- data/.github/workflows/continuous_integration.yml +0 -49
- data/.gitignore +0 -10
- data/Dockerfile.changelog-rs +0 -12
- data/Gemfile +0 -5
- data/ISSUE_TEMPLATE.md +0 -15
- data/PULL_REQUEST_TEMPLATE.md +0 -9
- data/RELEASING.md +0 -70
- data/Rakefile +0 -60
- data/lib/git/base/factory.rb +0 -99
- data/lib/git/base.rb +0 -711
- data/lib/git/command_line_result.rb +0 -86
- data/lib/git/failed_error.rb +0 -53
- data/lib/git/git_execute_error.rb +0 -7
- data/lib/git/index.rb +0 -5
- data/lib/git/lib.rb +0 -1328
- data/lib/git/path.rb +0 -31
- data/lib/git/signaled_error.rb +0 -50
- data/lib/git/working_directory.rb +0 -4
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'git/diff_result'
|
|
4
|
+
require 'git/diff_file_numstat_info'
|
|
5
|
+
require 'git/diff_file_raw_info'
|
|
6
|
+
require 'git/diff_file_patch_info'
|
|
7
|
+
require 'git/dirstat_info'
|
|
8
|
+
|
|
9
|
+
module Git
|
|
10
|
+
module Parsers
|
|
11
|
+
# Parser for git diff output in various formats
|
|
12
|
+
#
|
|
13
|
+
# Handles parsing of --numstat, --shortstat, --dirstat, --raw, and --patch output.
|
|
14
|
+
# This parser is used by stash show, diff, show, and log commands.
|
|
15
|
+
#
|
|
16
|
+
# @note Combined/merge diffs (e.g., from `git diff --cc` or `git show <merge>`) are not
|
|
17
|
+
# supported. These have a different format with multiple +/- columns per parent.
|
|
18
|
+
#
|
|
19
|
+
# ## Design Note: Namespace Organization
|
|
20
|
+
#
|
|
21
|
+
# This parser creates and returns {Git::DiffResult} and {Git::DiffFile*Info}
|
|
22
|
+
# objects, which live at the top-level `Git::` namespace rather than within
|
|
23
|
+
# `Git::Parsers::`. This is intentional:
|
|
24
|
+
#
|
|
25
|
+
# - **Parsers are infrastructure** - marked `@api private`, users shouldn't
|
|
26
|
+
# interact with them directly
|
|
27
|
+
# - **Info/Result classes are public API** - returned by commands and used
|
|
28
|
+
# throughout the codebase
|
|
29
|
+
# - **Info classes are domain entities** - represent git diff data
|
|
30
|
+
# - **Result classes are operation outcomes** - represent command results,
|
|
31
|
+
# not parsing details
|
|
32
|
+
#
|
|
33
|
+
# Keeping Info/Result classes at `Git::` improves discoverability and correctly
|
|
34
|
+
# reflects their role as public types rather than parser internals.
|
|
35
|
+
#
|
|
36
|
+
# @api private
|
|
37
|
+
#
|
|
38
|
+
module Diff
|
|
39
|
+
# Status letter to symbol mapping for --raw output
|
|
40
|
+
STATUS_MAP = {
|
|
41
|
+
'A' => :added,
|
|
42
|
+
'M' => :modified,
|
|
43
|
+
'D' => :deleted,
|
|
44
|
+
'R' => :renamed,
|
|
45
|
+
'C' => :copied,
|
|
46
|
+
'T' => :type_changed
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Null SHA for non-existent files
|
|
50
|
+
NULL_SHA = '0' * 7
|
|
51
|
+
|
|
52
|
+
# Null mode for non-existent files
|
|
53
|
+
NULL_MODE = '000000'
|
|
54
|
+
|
|
55
|
+
# Rename format patterns from git numstat -M output:
|
|
56
|
+
# old_name.rb => new_name.rb
|
|
57
|
+
# \\{old_dir => new_dir}/file.rb
|
|
58
|
+
# dir/\\{old_name.rb => new_name.rb}
|
|
59
|
+
RENAME_PATTERN = /\A(.+) => (.+)\z/
|
|
60
|
+
|
|
61
|
+
# Brace-expanded rename format from git numstat -M output
|
|
62
|
+
BRACE_RENAME_PATTERN = /\A(.*)\{(.+) => (.+)\}(.*)\z/
|
|
63
|
+
|
|
64
|
+
module_function
|
|
65
|
+
|
|
66
|
+
# Build a DiffResult from parsed components
|
|
67
|
+
#
|
|
68
|
+
# @param files [Array] array of file info objects
|
|
69
|
+
#
|
|
70
|
+
# @param shortstat [Hash] parsed shortstat data
|
|
71
|
+
#
|
|
72
|
+
# @param dirstat [Git::DirstatInfo, nil] parsed dirstat data
|
|
73
|
+
#
|
|
74
|
+
# @return [Git::DiffResult]
|
|
75
|
+
#
|
|
76
|
+
def build_result(files:, shortstat:, dirstat:)
|
|
77
|
+
Git::DiffResult.new(
|
|
78
|
+
files_changed: shortstat[:files_changed],
|
|
79
|
+
total_insertions: shortstat[:insertions],
|
|
80
|
+
total_deletions: shortstat[:deletions],
|
|
81
|
+
files: files,
|
|
82
|
+
dirstat: dirstat
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Build an empty DiffResult
|
|
87
|
+
#
|
|
88
|
+
# @return [Git::DiffResult]
|
|
89
|
+
#
|
|
90
|
+
def empty_result
|
|
91
|
+
Git::DiffResult.new(
|
|
92
|
+
files_changed: 0, total_insertions: 0, total_deletions: 0,
|
|
93
|
+
files: [], dirstat: nil
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Parse shortstat line into components
|
|
98
|
+
#
|
|
99
|
+
# @example
|
|
100
|
+
# parse_shortstat(" 3 files changed, 10 insertions(+), 5 deletions(-)")
|
|
101
|
+
# # => { files_changed: 3, insertions: 10, deletions: 5 }
|
|
102
|
+
#
|
|
103
|
+
# @param line [String, nil] the shortstat line
|
|
104
|
+
#
|
|
105
|
+
# @return [Hash] { files_changed:, insertions:, deletions: }
|
|
106
|
+
#
|
|
107
|
+
def parse_shortstat(line)
|
|
108
|
+
return { files_changed: 0, insertions: 0, deletions: 0 } if line.nil?
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
files_changed: line.match(/(\d+)\s+files?\s+changed/)&.[](1).to_i,
|
|
112
|
+
insertions: line.match(/(\d+)\s+insertions?\(\+\)/)&.[](1).to_i,
|
|
113
|
+
deletions: line.match(/(\d+)\s+deletions?\(-\)/)&.[](1).to_i
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Parse dirstat lines into DirstatInfo
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
# parse_dirstat([" 50.0% lib/", " 50.0% spec/"])
|
|
121
|
+
# # => #<Git::DirstatInfo entries: [...]>
|
|
122
|
+
#
|
|
123
|
+
# @param lines [Array<String>] dirstat output lines
|
|
124
|
+
#
|
|
125
|
+
# @return [Git::DirstatInfo]
|
|
126
|
+
#
|
|
127
|
+
def parse_dirstat(lines)
|
|
128
|
+
entries = lines.filter_map do |line|
|
|
129
|
+
next unless (match = line.match(/^\s*([\d.]+)%\s+(.+)$/))
|
|
130
|
+
|
|
131
|
+
Git::DirstatEntry.new(percentage: match[1].to_f, directory: match[2])
|
|
132
|
+
end
|
|
133
|
+
Git::DirstatInfo.new(entries: entries)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Parse a stat value (handles '-' for binary files)
|
|
137
|
+
#
|
|
138
|
+
# @param value [String] the stat value string
|
|
139
|
+
#
|
|
140
|
+
# @return [Integer] the numeric value (0 for binary files)
|
|
141
|
+
#
|
|
142
|
+
def parse_stat_value(value)
|
|
143
|
+
value == '-' ? 0 : value.to_i
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Unescape quoted path from git output
|
|
147
|
+
#
|
|
148
|
+
# @param path [String] potentially quoted path
|
|
149
|
+
#
|
|
150
|
+
# @return [String] unescaped path
|
|
151
|
+
#
|
|
152
|
+
def unescape_path(path)
|
|
153
|
+
return path unless path&.start_with?('"') && path.end_with?('"')
|
|
154
|
+
|
|
155
|
+
Git::EscapedPath.new(path[1..-2]).unescape
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Parser for --numstat output
|
|
159
|
+
module Numstat
|
|
160
|
+
module_function
|
|
161
|
+
|
|
162
|
+
# Parse numstat output into DiffResult
|
|
163
|
+
#
|
|
164
|
+
# @param output [String] raw numstat + shortstat output
|
|
165
|
+
#
|
|
166
|
+
# @param include_dirstat [Boolean] whether dirstat output is expected
|
|
167
|
+
#
|
|
168
|
+
# @return [Git::DiffResult]
|
|
169
|
+
#
|
|
170
|
+
def parse(output, include_dirstat: false)
|
|
171
|
+
lines = output.split("\n").reject(&:empty?)
|
|
172
|
+
numstat_lines, shortstat_line, dirstat_lines = split_sections(lines, include_dirstat)
|
|
173
|
+
|
|
174
|
+
Diff.build_result(
|
|
175
|
+
files: parse_file_stats(numstat_lines),
|
|
176
|
+
shortstat: Diff.parse_shortstat(shortstat_line),
|
|
177
|
+
dirstat: include_dirstat ? Diff.parse_dirstat(dirstat_lines) : nil
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Parse numstat lines into DiffFileNumstatInfo array
|
|
182
|
+
#
|
|
183
|
+
# @param lines [Array<String>] numstat lines
|
|
184
|
+
#
|
|
185
|
+
# @return [Array<Git::DiffFileNumstatInfo>]
|
|
186
|
+
#
|
|
187
|
+
def parse_file_stats(lines)
|
|
188
|
+
lines.map do |line|
|
|
189
|
+
insertions_s, deletions_s, filename = line.split("\t", 3)
|
|
190
|
+
path, src_path = parse_rename_path(filename)
|
|
191
|
+
Git::DiffFileNumstatInfo.new(
|
|
192
|
+
path: Diff.unescape_path(path),
|
|
193
|
+
src_path: src_path ? Diff.unescape_path(src_path) : nil,
|
|
194
|
+
insertions: Diff.parse_stat_value(insertions_s),
|
|
195
|
+
deletions: Diff.parse_stat_value(deletions_s)
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Parse numstat lines into a path -> stats hash (for combining with other formats)
|
|
201
|
+
#
|
|
202
|
+
# @param lines [Array<String>] numstat lines
|
|
203
|
+
#
|
|
204
|
+
# @param include_binary [Boolean] whether to include binary flag
|
|
205
|
+
#
|
|
206
|
+
# @return [Hash<String, Hash>] path to stats mapping (keyed by destination path)
|
|
207
|
+
#
|
|
208
|
+
def parse_as_map(lines, include_binary: false)
|
|
209
|
+
lines.to_h do |line|
|
|
210
|
+
insertions_s, deletions_s, filename = line.split("\t", 3)
|
|
211
|
+
# Normalize rename paths so the key matches the dst_path used by raw/patch parsers
|
|
212
|
+
dst_path, _src_path = parse_rename_path(filename)
|
|
213
|
+
[Diff.unescape_path(dst_path), build_stats(insertions_s, deletions_s, include_binary)]
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Builds stats data from a numstat entry
|
|
218
|
+
#
|
|
219
|
+
# @param insertions_s [String] insertions field from numstat output
|
|
220
|
+
#
|
|
221
|
+
# @param deletions_s [String] deletions field from numstat output
|
|
222
|
+
#
|
|
223
|
+
# @param include_binary [Boolean] whether to include the `:binary` key
|
|
224
|
+
#
|
|
225
|
+
# @return [Hash] stats hash with insertion and deletion totals
|
|
226
|
+
#
|
|
227
|
+
def build_stats(insertions_s, deletions_s, include_binary)
|
|
228
|
+
stats = { insertions: Diff.parse_stat_value(insertions_s),
|
|
229
|
+
deletions: Diff.parse_stat_value(deletions_s) }
|
|
230
|
+
stats[:binary] = (insertions_s == '-' && deletions_s == '-') if include_binary
|
|
231
|
+
stats
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Split output into numstat, shortstat, and dirstat sections
|
|
235
|
+
#
|
|
236
|
+
# @param lines [Array<String>] all output lines
|
|
237
|
+
#
|
|
238
|
+
# @param include_dirstat [Boolean] whether to expect dirstat section
|
|
239
|
+
#
|
|
240
|
+
# @return [Array] [numstat_lines, shortstat_line, dirstat_lines]
|
|
241
|
+
#
|
|
242
|
+
def split_sections(lines, include_dirstat)
|
|
243
|
+
shortstat_index = lines.index { |l| l.match?(/^\s*\d+\s+files?\s+changed/) }
|
|
244
|
+
return [lines, nil, []] unless shortstat_index
|
|
245
|
+
|
|
246
|
+
[lines[0...shortstat_index], lines[shortstat_index],
|
|
247
|
+
include_dirstat ? lines[(shortstat_index + 1)..] : []]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Parse potential rename path into [dst_path, src_path]
|
|
251
|
+
#
|
|
252
|
+
# @param filename [String] the path string from numstat output
|
|
253
|
+
#
|
|
254
|
+
# @return [Array(String, (String, nil))] [destination_path, source_path_or_nil]
|
|
255
|
+
#
|
|
256
|
+
def parse_rename_path(filename)
|
|
257
|
+
if (match = filename.match(BRACE_RENAME_PATTERN))
|
|
258
|
+
prefix, old_part, new_part, suffix = match.captures
|
|
259
|
+
["#{prefix}#{new_part}#{suffix}", "#{prefix}#{old_part}#{suffix}"]
|
|
260
|
+
elsif (match = filename.match(RENAME_PATTERN))
|
|
261
|
+
[match[2], match[1]]
|
|
262
|
+
else
|
|
263
|
+
[filename, nil]
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Parser for --raw output (combined with numstat)
|
|
269
|
+
module Raw
|
|
270
|
+
module_function
|
|
271
|
+
|
|
272
|
+
# Parse combined raw + numstat + shortstat output into DiffResult
|
|
273
|
+
#
|
|
274
|
+
# @param output [String] combined output
|
|
275
|
+
#
|
|
276
|
+
# @param include_dirstat [Boolean] whether dirstat output is expected
|
|
277
|
+
#
|
|
278
|
+
# @return [Git::DiffResult]
|
|
279
|
+
#
|
|
280
|
+
def parse(output, include_dirstat: false)
|
|
281
|
+
raw_lines, numstat_lines, shortstat_line, dirstat_lines = split_sections(output, include_dirstat)
|
|
282
|
+
numstat_map = Numstat.parse_as_map(numstat_lines, include_binary: true)
|
|
283
|
+
|
|
284
|
+
Diff.build_result(
|
|
285
|
+
files: raw_lines.map { |line| parse_raw_line(line, numstat_map) },
|
|
286
|
+
shortstat: Diff.parse_shortstat(shortstat_line),
|
|
287
|
+
dirstat: include_dirstat ? Diff.parse_dirstat(dirstat_lines) : nil
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Split output into raw, numstat, shortstat, and dirstat sections
|
|
292
|
+
#
|
|
293
|
+
# @param output [String] combined output
|
|
294
|
+
#
|
|
295
|
+
# @param include_dirstat [Boolean] whether to expect dirstat section
|
|
296
|
+
#
|
|
297
|
+
# @return [Array(Array<String>, Array<String>, (String, nil), Array<String>)]
|
|
298
|
+
#
|
|
299
|
+
def split_sections(output, include_dirstat)
|
|
300
|
+
lines = output.split("\n").reject(&:empty?)
|
|
301
|
+
raw_lines, non_raw_lines = lines.partition { |l| l.start_with?(':') }
|
|
302
|
+
shortstat_index = non_raw_lines.index { |l| l.match?(/^\s*\d+\s+files?\s+changed/) }
|
|
303
|
+
|
|
304
|
+
return [raw_lines, non_raw_lines, nil, []] unless shortstat_index
|
|
305
|
+
|
|
306
|
+
[raw_lines, non_raw_lines[0...shortstat_index], non_raw_lines[shortstat_index],
|
|
307
|
+
include_dirstat ? non_raw_lines[(shortstat_index + 1)..] : []]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Parse a single --raw output line
|
|
311
|
+
#
|
|
312
|
+
# @param line [String] a single raw output line
|
|
313
|
+
#
|
|
314
|
+
# @param numstat_map [Hash<String, Hash>] path to stats mapping
|
|
315
|
+
#
|
|
316
|
+
# @return [Git::DiffFileRawInfo]
|
|
317
|
+
#
|
|
318
|
+
def parse_raw_line(line, numstat_map)
|
|
319
|
+
parsed = parse_raw_line_parts(line)
|
|
320
|
+
stats = numstat_map.fetch(parsed[:dst_path] || parsed[:src_path],
|
|
321
|
+
{ insertions: 0, deletions: 0, binary: false })
|
|
322
|
+
build_raw_info(parsed, stats)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Parses the fields from a raw diff line
|
|
326
|
+
#
|
|
327
|
+
# @param line [String] a single raw output line
|
|
328
|
+
#
|
|
329
|
+
# @return [Hash] parsed metadata for one raw diff entry
|
|
330
|
+
#
|
|
331
|
+
def parse_raw_line_parts(line)
|
|
332
|
+
parts = line[1..].split(/\s+/, 5)
|
|
333
|
+
status_char, *paths = parts[4].split("\t")
|
|
334
|
+
status, similarity = parse_status(status_char)
|
|
335
|
+
src_path, dst_path = extract_paths(paths)
|
|
336
|
+
{ modes: parts[0..1], shas: parts[2..3], status: status,
|
|
337
|
+
similarity: similarity, src_path: src_path, dst_path: dst_path }
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Builds a raw diff file info object from parsed metadata and stats
|
|
341
|
+
#
|
|
342
|
+
# @param parsed [Hash] parsed metadata from {#parse_raw_line_parts}
|
|
343
|
+
#
|
|
344
|
+
# @param stats [Hash] insertion, deletion, and binary information
|
|
345
|
+
#
|
|
346
|
+
# @return [Git::DiffFileRawInfo]
|
|
347
|
+
#
|
|
348
|
+
def build_raw_info(parsed, stats)
|
|
349
|
+
Git::DiffFileRawInfo.new(
|
|
350
|
+
src: build_file_ref(parsed[:modes][0], parsed[:shas][0], parsed[:src_path]),
|
|
351
|
+
dst: build_file_ref(parsed[:modes][1], parsed[:shas][1], parsed[:dst_path]),
|
|
352
|
+
status: parsed[:status], similarity: parsed[:similarity], **stats
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Parse status character and optional similarity percentage
|
|
357
|
+
#
|
|
358
|
+
# @param status_char [String] e.g., 'M', 'A', 'R075'
|
|
359
|
+
#
|
|
360
|
+
# @return [Array(Symbol, (Integer, nil))] [status, similarity]
|
|
361
|
+
#
|
|
362
|
+
def parse_status(status_char)
|
|
363
|
+
letter = status_char[0]
|
|
364
|
+
similarity = status_char.length > 1 ? status_char[1..].to_i : nil
|
|
365
|
+
[STATUS_MAP.fetch(letter, :unknown), similarity]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Extract source and destination paths from raw output paths
|
|
369
|
+
#
|
|
370
|
+
# @param paths [Array<String>] paths array
|
|
371
|
+
#
|
|
372
|
+
# @return [Array((String, nil), (String, nil))] [src_path, dst_path]
|
|
373
|
+
#
|
|
374
|
+
def extract_paths(paths)
|
|
375
|
+
if paths.length == 2
|
|
376
|
+
[Diff.unescape_path(paths[0]), Diff.unescape_path(paths[1])]
|
|
377
|
+
else
|
|
378
|
+
path = Diff.unescape_path(paths[0])
|
|
379
|
+
[path, path]
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Build a FileRef, returning nil if the file doesn't exist on this side
|
|
384
|
+
#
|
|
385
|
+
# @param mode [String] file mode
|
|
386
|
+
#
|
|
387
|
+
# @param sha [String] file SHA
|
|
388
|
+
#
|
|
389
|
+
# @param path [String, nil] file path
|
|
390
|
+
#
|
|
391
|
+
# @return [Git::FileRef, nil]
|
|
392
|
+
#
|
|
393
|
+
def build_file_ref(mode, sha, path)
|
|
394
|
+
return nil if mode == NULL_MODE || path.nil?
|
|
395
|
+
|
|
396
|
+
Git::FileRef.new(mode: mode, sha: sha, path: path)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Parser for --patch output (combined with numstat)
|
|
401
|
+
module Patch
|
|
402
|
+
# Header line pattern for each patch file section
|
|
403
|
+
DIFF_HEADER_PATTERN = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}
|
|
404
|
+
|
|
405
|
+
# Index line pattern with source/destination SHAs and optional mode
|
|
406
|
+
INDEX_PATTERN = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)?/
|
|
407
|
+
|
|
408
|
+
# File mode line pattern for file creation and deletion
|
|
409
|
+
FILE_MODE_PATTERN = /^(new|deleted) file mode (......)/
|
|
410
|
+
|
|
411
|
+
# Old mode line pattern for mode changes
|
|
412
|
+
OLD_MODE_PATTERN = /^old mode (......)/
|
|
413
|
+
|
|
414
|
+
# New mode line pattern for mode changes
|
|
415
|
+
NEW_MODE_PATTERN = /^new mode (......)/
|
|
416
|
+
|
|
417
|
+
# Binary patch marker line pattern
|
|
418
|
+
BINARY_PATTERN = /^Binary files /
|
|
419
|
+
|
|
420
|
+
# Git binary patch header line pattern
|
|
421
|
+
GIT_BINARY_PATCH_PATTERN = /^GIT binary patch$/
|
|
422
|
+
|
|
423
|
+
# Rename source path line pattern
|
|
424
|
+
RENAME_FROM_PATTERN = /^rename from (.+)$/
|
|
425
|
+
|
|
426
|
+
# Rename destination path line pattern
|
|
427
|
+
RENAME_TO_PATTERN = /^rename to (.+)$/
|
|
428
|
+
|
|
429
|
+
# Copy source path line pattern
|
|
430
|
+
COPY_FROM_PATTERN = /^copy from (.+)$/
|
|
431
|
+
|
|
432
|
+
# Copy destination path line pattern
|
|
433
|
+
COPY_TO_PATTERN = /^copy to (.+)$/
|
|
434
|
+
|
|
435
|
+
# Similarity percentage line pattern
|
|
436
|
+
SIMILARITY_PATTERN = /^similarity index (\d+)%$/
|
|
437
|
+
|
|
438
|
+
# Maps patch metadata status words to diff status symbols
|
|
439
|
+
PATCH_STATUS_MAP = {
|
|
440
|
+
'new' => :added,
|
|
441
|
+
'deleted' => :deleted,
|
|
442
|
+
'modified' => :modified,
|
|
443
|
+
'renamed' => :renamed,
|
|
444
|
+
'copied' => :copied,
|
|
445
|
+
'type_changed' => :type_changed
|
|
446
|
+
}.freeze
|
|
447
|
+
|
|
448
|
+
module_function
|
|
449
|
+
|
|
450
|
+
# Parse combined patch + numstat + shortstat output into DiffResult
|
|
451
|
+
#
|
|
452
|
+
# @param output [String] combined output
|
|
453
|
+
#
|
|
454
|
+
# @param include_dirstat [Boolean] whether dirstat output is expected
|
|
455
|
+
#
|
|
456
|
+
# @return [Git::DiffResult]
|
|
457
|
+
#
|
|
458
|
+
def parse(output, include_dirstat: false)
|
|
459
|
+
return Diff.empty_result if output.empty?
|
|
460
|
+
|
|
461
|
+
numstat_lines, shortstat_line, dirstat_lines, patch_text = split_sections(output, include_dirstat)
|
|
462
|
+
numstat_map = Numstat.parse_as_map(numstat_lines)
|
|
463
|
+
|
|
464
|
+
Diff.build_result(
|
|
465
|
+
files: PatchFileParser.new(patch_text, numstat_map).parse,
|
|
466
|
+
shortstat: Diff.parse_shortstat(shortstat_line),
|
|
467
|
+
dirstat: include_dirstat ? Diff.parse_dirstat(dirstat_lines) : nil
|
|
468
|
+
)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Split output into numstat, shortstat, dirstat, and patch sections
|
|
472
|
+
#
|
|
473
|
+
# @param output [String] combined output
|
|
474
|
+
#
|
|
475
|
+
# @param include_dirstat [Boolean] whether to expect dirstat section
|
|
476
|
+
#
|
|
477
|
+
# @return [Array(Array<String>, (String, nil), Array<String>, String)]
|
|
478
|
+
#
|
|
479
|
+
def split_sections(output, include_dirstat)
|
|
480
|
+
lines = output.lines
|
|
481
|
+
first_diff_index = lines.index { |l| l.start_with?('diff --git') } || lines.length
|
|
482
|
+
pre_diff_lines = lines[0...first_diff_index].map(&:chomp).reject(&:empty?)
|
|
483
|
+
patch_text = lines[first_diff_index..].join
|
|
484
|
+
split_pre_diff(pre_diff_lines, include_dirstat, patch_text)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Splits pre-patch lines into numstat, shortstat, and dirstat sections
|
|
488
|
+
#
|
|
489
|
+
# @param pre_diff_lines [Array<String>] lines before the first diff header
|
|
490
|
+
#
|
|
491
|
+
# @param include_dirstat [Boolean] whether dirstat output is expected
|
|
492
|
+
#
|
|
493
|
+
# @param patch_text [String] patch text starting at the first diff header
|
|
494
|
+
#
|
|
495
|
+
# @return [Array(Array<String>, (String, nil), Array<String>, String)]
|
|
496
|
+
#
|
|
497
|
+
def split_pre_diff(pre_diff_lines, include_dirstat, patch_text)
|
|
498
|
+
shortstat_index = pre_diff_lines.index { |l| l.match?(/^\s*\d+\s+files?\s+changed/) }
|
|
499
|
+
return [pre_diff_lines, nil, [], patch_text] unless shortstat_index
|
|
500
|
+
|
|
501
|
+
[pre_diff_lines[0...shortstat_index], pre_diff_lines[shortstat_index],
|
|
502
|
+
include_dirstat ? pre_diff_lines[(shortstat_index + 1)..] : [], patch_text]
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Methods for parsing patch metadata lines (index, mode, rename, etc.)
|
|
506
|
+
#
|
|
507
|
+
# @api private
|
|
508
|
+
module PatchMetadataParser
|
|
509
|
+
private
|
|
510
|
+
|
|
511
|
+
# Parses a metadata line for the current patch file
|
|
512
|
+
#
|
|
513
|
+
# @param line [String] a metadata line from the patch block
|
|
514
|
+
#
|
|
515
|
+
# @return [void]
|
|
516
|
+
#
|
|
517
|
+
def parse_metadata_line(line)
|
|
518
|
+
try_parse_index(line)
|
|
519
|
+
try_parse_file_mode(line)
|
|
520
|
+
try_parse_old_new_mode(line)
|
|
521
|
+
try_parse_rename(line)
|
|
522
|
+
try_parse_similarity(line)
|
|
523
|
+
try_mark_binary(line)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Parses index metadata and updates mode defaults when present
|
|
527
|
+
#
|
|
528
|
+
# @param line [String] a metadata line from the patch block
|
|
529
|
+
#
|
|
530
|
+
# @return [void]
|
|
531
|
+
#
|
|
532
|
+
def try_parse_index(line)
|
|
533
|
+
return unless (match = line.match(INDEX_PATTERN))
|
|
534
|
+
|
|
535
|
+
@current_file[:src_sha] = match[1]
|
|
536
|
+
@current_file[:dst_sha] = match[2]
|
|
537
|
+
return unless (mode = match[3]&.strip)
|
|
538
|
+
return unless @current_file[:src_mode].nil? && @current_file[:dst_mode].nil?
|
|
539
|
+
|
|
540
|
+
@current_file[:src_mode] = @current_file[:dst_mode] = mode
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Parses file creation and deletion mode lines
|
|
544
|
+
#
|
|
545
|
+
# @param line [String] a metadata line from the patch block
|
|
546
|
+
#
|
|
547
|
+
# @return [void]
|
|
548
|
+
#
|
|
549
|
+
def try_parse_file_mode(line)
|
|
550
|
+
return unless (match = line.match(FILE_MODE_PATTERN))
|
|
551
|
+
|
|
552
|
+
type, mode = match.captures
|
|
553
|
+
@current_file[:status] = PATCH_STATUS_MAP.fetch(type, :modified)
|
|
554
|
+
apply_file_mode(type, mode)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Parses old/new mode lines and detects type changes
|
|
558
|
+
#
|
|
559
|
+
# @param line [String] a metadata line from the patch block
|
|
560
|
+
#
|
|
561
|
+
# @return [void]
|
|
562
|
+
#
|
|
563
|
+
def try_parse_old_new_mode(line)
|
|
564
|
+
if (match = line.match(OLD_MODE_PATTERN))
|
|
565
|
+
@current_file[:src_mode] = match[1]
|
|
566
|
+
detect_type_change
|
|
567
|
+
elsif (match = line.match(NEW_MODE_PATTERN))
|
|
568
|
+
@current_file[:dst_mode] = match[1]
|
|
569
|
+
detect_type_change
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Marks the current entry as a type change when type bits differ
|
|
574
|
+
#
|
|
575
|
+
# @return [void]
|
|
576
|
+
#
|
|
577
|
+
def detect_type_change
|
|
578
|
+
src_mode = @current_file[:src_mode]
|
|
579
|
+
dst_mode = @current_file[:dst_mode]
|
|
580
|
+
return unless src_mode && dst_mode
|
|
581
|
+
|
|
582
|
+
# Type change occurs when the file type bits differ (e.g., 100644 vs 120000)
|
|
583
|
+
# The first 3 digits represent the file type
|
|
584
|
+
@current_file[:status] = :type_changed if src_mode[0, 3] != dst_mode[0, 3]
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
# Applies source or destination mode updates based on file operation type
|
|
588
|
+
#
|
|
589
|
+
# @param type [String] operation type (`'new'` or `'deleted'`)
|
|
590
|
+
#
|
|
591
|
+
# @param mode [String] file mode from patch metadata
|
|
592
|
+
#
|
|
593
|
+
# @return [void]
|
|
594
|
+
#
|
|
595
|
+
def apply_file_mode(type, mode)
|
|
596
|
+
if type == 'new'
|
|
597
|
+
@current_file[:dst_mode] = mode
|
|
598
|
+
@current_file[:src_path] = nil
|
|
599
|
+
else
|
|
600
|
+
@current_file[:src_mode] = mode
|
|
601
|
+
@current_file[:dst_path] = nil
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Parses rename or copy metadata for the current patch file
|
|
606
|
+
#
|
|
607
|
+
# @param line [String] a metadata line from the patch block
|
|
608
|
+
#
|
|
609
|
+
# @return [Symbol, nil] parsed status when a rename or copy line matches
|
|
610
|
+
#
|
|
611
|
+
def try_parse_rename(line)
|
|
612
|
+
try_parse_rename_or_copy(line, RENAME_FROM_PATTERN, RENAME_TO_PATTERN, :renamed) ||
|
|
613
|
+
try_parse_rename_or_copy(line, COPY_FROM_PATTERN, COPY_TO_PATTERN, :copied)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Parses one side of rename or copy metadata
|
|
617
|
+
#
|
|
618
|
+
# @param line [String] a metadata line from the patch block
|
|
619
|
+
#
|
|
620
|
+
# @param from_pattern [Regexp] pattern for source path lines
|
|
621
|
+
#
|
|
622
|
+
# @param to_pattern [Regexp] pattern for destination path lines
|
|
623
|
+
#
|
|
624
|
+
# @param status [Symbol] status to set when a pattern matches
|
|
625
|
+
#
|
|
626
|
+
# @return [Symbol, nil] the assigned status when matched
|
|
627
|
+
#
|
|
628
|
+
def try_parse_rename_or_copy(line, from_pattern, to_pattern, status)
|
|
629
|
+
if (match = line.match(from_pattern))
|
|
630
|
+
@current_file[:src_path] = Diff.unescape_path(match[1])
|
|
631
|
+
@current_file[:status] = status
|
|
632
|
+
elsif (match = line.match(to_pattern))
|
|
633
|
+
@current_file[:dst_path] = Diff.unescape_path(match[1])
|
|
634
|
+
@current_file[:status] = status
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Parses similarity index metadata
|
|
639
|
+
#
|
|
640
|
+
# @param line [String] a metadata line from the patch block
|
|
641
|
+
#
|
|
642
|
+
# @return [Integer, nil] parsed similarity percentage when matched
|
|
643
|
+
#
|
|
644
|
+
def try_parse_similarity(line)
|
|
645
|
+
return unless (match = line.match(SIMILARITY_PATTERN))
|
|
646
|
+
|
|
647
|
+
@current_file[:similarity] = match[1].to_i
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Marks the current file as binary when binary markers are present
|
|
651
|
+
#
|
|
652
|
+
# @param line [String] a metadata line from the patch block
|
|
653
|
+
#
|
|
654
|
+
# @return [Boolean, nil] true when binary metadata is detected
|
|
655
|
+
#
|
|
656
|
+
def try_mark_binary(line)
|
|
657
|
+
@current_file[:binary] = true if line.match?(BINARY_PATTERN) || line.match?(GIT_BINARY_PATCH_PATTERN)
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Stateful parser for unified diff patch output
|
|
662
|
+
#
|
|
663
|
+
# @api private
|
|
664
|
+
class PatchFileParser
|
|
665
|
+
include PatchMetadataParser
|
|
666
|
+
|
|
667
|
+
# Initializes parser state for a patch text block
|
|
668
|
+
#
|
|
669
|
+
# @param patch_text [String] unified diff patch text
|
|
670
|
+
#
|
|
671
|
+
# @param numstat_map [Hash<String, Hash>] path to insert/delete stats
|
|
672
|
+
#
|
|
673
|
+
# @return [void]
|
|
674
|
+
#
|
|
675
|
+
def initialize(patch_text, numstat_map = {})
|
|
676
|
+
@patch_text = patch_text
|
|
677
|
+
@numstat_map = numstat_map
|
|
678
|
+
@files = []
|
|
679
|
+
@current_file = nil
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# Parses patch text into file patch info objects
|
|
683
|
+
#
|
|
684
|
+
# @return [Array<Git::DiffFilePatchInfo>]
|
|
685
|
+
#
|
|
686
|
+
def parse
|
|
687
|
+
@patch_text.split("\n").each { |line| process_line(line) }
|
|
688
|
+
finalize_current_file
|
|
689
|
+
@files
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
private
|
|
693
|
+
|
|
694
|
+
# Processes one patch line and updates parser state
|
|
695
|
+
#
|
|
696
|
+
# @param line [String] a single line from patch text
|
|
697
|
+
#
|
|
698
|
+
# @return [void]
|
|
699
|
+
#
|
|
700
|
+
def process_line(line)
|
|
701
|
+
if (match = line.match(DIFF_HEADER_PATTERN))
|
|
702
|
+
start_new_file(match, line)
|
|
703
|
+
elsif @current_file
|
|
704
|
+
append_to_current_file(line)
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Starts a new file section from a `diff --git` header
|
|
709
|
+
#
|
|
710
|
+
# @param match [MatchData] parsed header match for current diff file
|
|
711
|
+
#
|
|
712
|
+
# @param line [String] raw `diff --git` header line
|
|
713
|
+
#
|
|
714
|
+
# @return [void]
|
|
715
|
+
#
|
|
716
|
+
def start_new_file(match, line)
|
|
717
|
+
finalize_current_file
|
|
718
|
+
@current_file = default_file_state.merge(
|
|
719
|
+
patch: line,
|
|
720
|
+
src_path: Git::EscapedPath.new(match[2]).unescape,
|
|
721
|
+
dst_path: Git::EscapedPath.new(match[4]).unescape
|
|
722
|
+
)
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Builds the default state hash for a new patch file entry
|
|
726
|
+
#
|
|
727
|
+
# @return [Hash] default metadata for an in-progress file parse
|
|
728
|
+
#
|
|
729
|
+
def default_file_state
|
|
730
|
+
{ src_mode: nil, dst_mode: nil, src_sha: '', dst_sha: '',
|
|
731
|
+
src_path: nil, dst_path: nil, status: :modified, similarity: nil, binary: false }
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Appends metadata and patch text to the current file entry
|
|
735
|
+
#
|
|
736
|
+
# @param line [String] a metadata or patch content line
|
|
737
|
+
#
|
|
738
|
+
# @return [void]
|
|
739
|
+
#
|
|
740
|
+
def append_to_current_file(line)
|
|
741
|
+
parse_metadata_line(line)
|
|
742
|
+
@current_file[:patch] = "#{@current_file[:patch]}\n#{line}"
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Finalizes and stores the current file entry
|
|
746
|
+
#
|
|
747
|
+
# @return [void]
|
|
748
|
+
#
|
|
749
|
+
def finalize_current_file
|
|
750
|
+
return unless @current_file
|
|
751
|
+
|
|
752
|
+
@files << build_patch_info
|
|
753
|
+
@current_file = nil
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# Builds a finalized patch info object for the current file
|
|
757
|
+
#
|
|
758
|
+
# @return [Git::DiffFilePatchInfo]
|
|
759
|
+
#
|
|
760
|
+
def build_patch_info
|
|
761
|
+
path = @current_file[:dst_path] || @current_file[:src_path]
|
|
762
|
+
stats = @numstat_map.fetch(path, { insertions: 0, deletions: 0 })
|
|
763
|
+
|
|
764
|
+
Git::DiffFilePatchInfo.new(
|
|
765
|
+
src: build_file_ref(:src), dst: build_file_ref(:dst),
|
|
766
|
+
patch: @current_file[:patch], status: @current_file[:status],
|
|
767
|
+
similarity: @current_file[:similarity], binary: @current_file[:binary],
|
|
768
|
+
insertions: stats[:insertions], deletions: stats[:deletions]
|
|
769
|
+
)
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
# Builds a file reference for one side of the diff entry
|
|
773
|
+
#
|
|
774
|
+
# @param side [Symbol] diff side key (`:src` or `:dst`)
|
|
775
|
+
#
|
|
776
|
+
# @return [Git::FileRef, nil]
|
|
777
|
+
#
|
|
778
|
+
def build_file_ref(side)
|
|
779
|
+
path = @current_file[:"#{side}_path"]
|
|
780
|
+
return nil if path.nil?
|
|
781
|
+
|
|
782
|
+
Git::FileRef.new(
|
|
783
|
+
mode: @current_file[:"#{side}_mode"] || '',
|
|
784
|
+
sha: @current_file[:"#{side}_sha"] || '',
|
|
785
|
+
path: path
|
|
786
|
+
)
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
end
|