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,585 @@
|
|
|
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
|
+
BRACE_RENAME_PATTERN = /\A(.*)\{(.+) => (.+)\}(.*)\z/
|
|
61
|
+
|
|
62
|
+
module_function
|
|
63
|
+
|
|
64
|
+
# Build a DiffResult from parsed components
|
|
65
|
+
#
|
|
66
|
+
# @param files [Array] array of file info objects
|
|
67
|
+
# @param shortstat [Hash] parsed shortstat data
|
|
68
|
+
# @param dirstat [Git::DirstatInfo, nil] parsed dirstat data
|
|
69
|
+
# @return [Git::DiffResult]
|
|
70
|
+
#
|
|
71
|
+
def build_result(files:, shortstat:, dirstat:)
|
|
72
|
+
Git::DiffResult.new(
|
|
73
|
+
files_changed: shortstat[:files_changed],
|
|
74
|
+
total_insertions: shortstat[:insertions],
|
|
75
|
+
total_deletions: shortstat[:deletions],
|
|
76
|
+
files: files,
|
|
77
|
+
dirstat: dirstat
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Build an empty DiffResult
|
|
82
|
+
#
|
|
83
|
+
# @return [Git::DiffResult]
|
|
84
|
+
#
|
|
85
|
+
def empty_result
|
|
86
|
+
Git::DiffResult.new(
|
|
87
|
+
files_changed: 0, total_insertions: 0, total_deletions: 0,
|
|
88
|
+
files: [], dirstat: nil
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Parse shortstat line into components
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# parse_shortstat(" 3 files changed, 10 insertions(+), 5 deletions(-)")
|
|
96
|
+
# # => { files_changed: 3, insertions: 10, deletions: 5 }
|
|
97
|
+
#
|
|
98
|
+
# @param line [String, nil] the shortstat line
|
|
99
|
+
# @return [Hash] { files_changed:, insertions:, deletions: }
|
|
100
|
+
#
|
|
101
|
+
def parse_shortstat(line)
|
|
102
|
+
return { files_changed: 0, insertions: 0, deletions: 0 } if line.nil?
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
files_changed: line.match(/(\d+)\s+files?\s+changed/)&.[](1).to_i,
|
|
106
|
+
insertions: line.match(/(\d+)\s+insertions?\(\+\)/)&.[](1).to_i,
|
|
107
|
+
deletions: line.match(/(\d+)\s+deletions?\(-\)/)&.[](1).to_i
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Parse dirstat lines into DirstatInfo
|
|
112
|
+
#
|
|
113
|
+
# @example
|
|
114
|
+
# parse_dirstat([" 50.0% lib/", " 50.0% spec/"])
|
|
115
|
+
# # => #<Git::DirstatInfo entries: [...]>
|
|
116
|
+
#
|
|
117
|
+
# @param lines [Array<String>] dirstat output lines
|
|
118
|
+
# @return [Git::DirstatInfo]
|
|
119
|
+
#
|
|
120
|
+
def parse_dirstat(lines)
|
|
121
|
+
entries = lines.filter_map do |line|
|
|
122
|
+
next unless (match = line.match(/^\s*([\d.]+)%\s+(.+)$/))
|
|
123
|
+
|
|
124
|
+
Git::DirstatEntry.new(percentage: match[1].to_f, directory: match[2])
|
|
125
|
+
end
|
|
126
|
+
Git::DirstatInfo.new(entries: entries)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Parse a stat value (handles '-' for binary files)
|
|
130
|
+
#
|
|
131
|
+
# @param value [String] the stat value string
|
|
132
|
+
# @return [Integer] the numeric value (0 for binary files)
|
|
133
|
+
#
|
|
134
|
+
def parse_stat_value(value)
|
|
135
|
+
value == '-' ? 0 : value.to_i
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Unescape quoted path from git output
|
|
139
|
+
#
|
|
140
|
+
# @param path [String] potentially quoted path
|
|
141
|
+
# @return [String] unescaped path
|
|
142
|
+
#
|
|
143
|
+
def unescape_path(path)
|
|
144
|
+
return path unless path&.start_with?('"') && path.end_with?('"')
|
|
145
|
+
|
|
146
|
+
Git::EscapedPath.new(path[1..-2]).unescape
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Parser for --numstat output
|
|
150
|
+
module Numstat
|
|
151
|
+
module_function
|
|
152
|
+
|
|
153
|
+
# Parse numstat output into DiffResult
|
|
154
|
+
#
|
|
155
|
+
# @param output [String] raw numstat + shortstat output
|
|
156
|
+
# @param include_dirstat [Boolean] whether dirstat output is expected
|
|
157
|
+
# @return [Git::DiffResult]
|
|
158
|
+
#
|
|
159
|
+
def parse(output, include_dirstat: false)
|
|
160
|
+
lines = output.split("\n").reject(&:empty?)
|
|
161
|
+
numstat_lines, shortstat_line, dirstat_lines = split_sections(lines, include_dirstat)
|
|
162
|
+
|
|
163
|
+
Diff.build_result(
|
|
164
|
+
files: parse_file_stats(numstat_lines),
|
|
165
|
+
shortstat: Diff.parse_shortstat(shortstat_line),
|
|
166
|
+
dirstat: include_dirstat ? Diff.parse_dirstat(dirstat_lines) : nil
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Parse numstat lines into DiffFileNumstatInfo array
|
|
171
|
+
#
|
|
172
|
+
# @param lines [Array<String>] numstat lines
|
|
173
|
+
# @return [Array<Git::DiffFileNumstatInfo>]
|
|
174
|
+
#
|
|
175
|
+
def parse_file_stats(lines)
|
|
176
|
+
lines.map do |line|
|
|
177
|
+
insertions_s, deletions_s, filename = line.split("\t", 3)
|
|
178
|
+
path, src_path = parse_rename_path(filename)
|
|
179
|
+
Git::DiffFileNumstatInfo.new(
|
|
180
|
+
path: Diff.unescape_path(path),
|
|
181
|
+
src_path: src_path ? Diff.unescape_path(src_path) : nil,
|
|
182
|
+
insertions: Diff.parse_stat_value(insertions_s),
|
|
183
|
+
deletions: Diff.parse_stat_value(deletions_s)
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Parse numstat lines into a path -> stats hash (for combining with other formats)
|
|
189
|
+
#
|
|
190
|
+
# @param lines [Array<String>] numstat lines
|
|
191
|
+
# @param include_binary [Boolean] whether to include binary flag
|
|
192
|
+
# @return [Hash<String, Hash>] path to stats mapping (keyed by destination path)
|
|
193
|
+
#
|
|
194
|
+
def parse_as_map(lines, include_binary: false)
|
|
195
|
+
lines.to_h do |line|
|
|
196
|
+
insertions_s, deletions_s, filename = line.split("\t", 3)
|
|
197
|
+
# Normalize rename paths so the key matches the dst_path used by raw/patch parsers
|
|
198
|
+
dst_path, _src_path = parse_rename_path(filename)
|
|
199
|
+
[Diff.unescape_path(dst_path), build_stats(insertions_s, deletions_s, include_binary)]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def build_stats(insertions_s, deletions_s, include_binary)
|
|
204
|
+
stats = { insertions: Diff.parse_stat_value(insertions_s),
|
|
205
|
+
deletions: Diff.parse_stat_value(deletions_s) }
|
|
206
|
+
stats[:binary] = (insertions_s == '-' && deletions_s == '-') if include_binary
|
|
207
|
+
stats
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Split output into numstat, shortstat, and dirstat sections
|
|
211
|
+
#
|
|
212
|
+
# @param lines [Array<String>] all output lines
|
|
213
|
+
# @param include_dirstat [Boolean] whether to expect dirstat section
|
|
214
|
+
# @return [Array] [numstat_lines, shortstat_line, dirstat_lines]
|
|
215
|
+
#
|
|
216
|
+
def split_sections(lines, include_dirstat)
|
|
217
|
+
shortstat_index = lines.index { |l| l.match?(/^\s*\d+\s+files?\s+changed/) }
|
|
218
|
+
return [lines, nil, []] unless shortstat_index
|
|
219
|
+
|
|
220
|
+
[lines[0...shortstat_index], lines[shortstat_index],
|
|
221
|
+
include_dirstat ? lines[(shortstat_index + 1)..] : []]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Parse potential rename path into [dst_path, src_path]
|
|
225
|
+
#
|
|
226
|
+
# @param filename [String] the path string from numstat output
|
|
227
|
+
# @return [Array<String, String|nil>] [destination_path, source_path_or_nil]
|
|
228
|
+
#
|
|
229
|
+
def parse_rename_path(filename)
|
|
230
|
+
if (match = filename.match(BRACE_RENAME_PATTERN))
|
|
231
|
+
prefix, old_part, new_part, suffix = match.captures
|
|
232
|
+
["#{prefix}#{new_part}#{suffix}", "#{prefix}#{old_part}#{suffix}"]
|
|
233
|
+
elsif (match = filename.match(RENAME_PATTERN))
|
|
234
|
+
[match[2], match[1]]
|
|
235
|
+
else
|
|
236
|
+
[filename, nil]
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Parser for --raw output (combined with numstat)
|
|
242
|
+
module Raw
|
|
243
|
+
module_function
|
|
244
|
+
|
|
245
|
+
# Parse combined raw + numstat + shortstat output into DiffResult
|
|
246
|
+
#
|
|
247
|
+
# @param output [String] combined output
|
|
248
|
+
# @param include_dirstat [Boolean] whether dirstat output is expected
|
|
249
|
+
# @return [Git::DiffResult]
|
|
250
|
+
#
|
|
251
|
+
def parse(output, include_dirstat: false)
|
|
252
|
+
raw_lines, numstat_lines, shortstat_line, dirstat_lines = split_sections(output, include_dirstat)
|
|
253
|
+
numstat_map = Numstat.parse_as_map(numstat_lines, include_binary: true)
|
|
254
|
+
|
|
255
|
+
Diff.build_result(
|
|
256
|
+
files: raw_lines.map { |line| parse_raw_line(line, numstat_map) },
|
|
257
|
+
shortstat: Diff.parse_shortstat(shortstat_line),
|
|
258
|
+
dirstat: include_dirstat ? Diff.parse_dirstat(dirstat_lines) : nil
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Split output into raw, numstat, shortstat, and dirstat sections
|
|
263
|
+
#
|
|
264
|
+
# @param output [String] combined output
|
|
265
|
+
# @param include_dirstat [Boolean] whether to expect dirstat section
|
|
266
|
+
# @return [Array<Array<String>, Array<String>, String|nil, Array<String>>]
|
|
267
|
+
#
|
|
268
|
+
def split_sections(output, include_dirstat)
|
|
269
|
+
lines = output.split("\n").reject(&:empty?)
|
|
270
|
+
raw_lines, non_raw_lines = lines.partition { |l| l.start_with?(':') }
|
|
271
|
+
shortstat_index = non_raw_lines.index { |l| l.match?(/^\s*\d+\s+files?\s+changed/) }
|
|
272
|
+
|
|
273
|
+
return [raw_lines, non_raw_lines, nil, []] unless shortstat_index
|
|
274
|
+
|
|
275
|
+
[raw_lines, non_raw_lines[0...shortstat_index], non_raw_lines[shortstat_index],
|
|
276
|
+
include_dirstat ? non_raw_lines[(shortstat_index + 1)..] : []]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Parse a single --raw output line
|
|
280
|
+
#
|
|
281
|
+
# @param line [String] a single raw output line
|
|
282
|
+
# @param numstat_map [Hash<String, Hash>] path to stats mapping
|
|
283
|
+
# @return [Git::DiffFileRawInfo]
|
|
284
|
+
#
|
|
285
|
+
def parse_raw_line(line, numstat_map)
|
|
286
|
+
parsed = parse_raw_line_parts(line)
|
|
287
|
+
stats = numstat_map.fetch(parsed[:dst_path] || parsed[:src_path],
|
|
288
|
+
{ insertions: 0, deletions: 0, binary: false })
|
|
289
|
+
build_raw_info(parsed, stats)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def parse_raw_line_parts(line)
|
|
293
|
+
parts = line[1..].split(/\s+/, 5)
|
|
294
|
+
status_char, *paths = parts[4].split("\t")
|
|
295
|
+
status, similarity = parse_status(status_char)
|
|
296
|
+
src_path, dst_path = extract_paths(paths)
|
|
297
|
+
{ modes: parts[0..1], shas: parts[2..3], status: status,
|
|
298
|
+
similarity: similarity, src_path: src_path, dst_path: dst_path }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def build_raw_info(parsed, stats)
|
|
302
|
+
Git::DiffFileRawInfo.new(
|
|
303
|
+
src: build_file_ref(parsed[:modes][0], parsed[:shas][0], parsed[:src_path]),
|
|
304
|
+
dst: build_file_ref(parsed[:modes][1], parsed[:shas][1], parsed[:dst_path]),
|
|
305
|
+
status: parsed[:status], similarity: parsed[:similarity], **stats
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Parse status character and optional similarity percentage
|
|
310
|
+
#
|
|
311
|
+
# @param status_char [String] e.g., 'M', 'A', 'R075'
|
|
312
|
+
# @return [Array<Symbol, Integer|nil>] [status, similarity]
|
|
313
|
+
#
|
|
314
|
+
def parse_status(status_char)
|
|
315
|
+
letter = status_char[0]
|
|
316
|
+
similarity = status_char.length > 1 ? status_char[1..].to_i : nil
|
|
317
|
+
[STATUS_MAP.fetch(letter, :unknown), similarity]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Extract source and destination paths from raw output paths
|
|
321
|
+
#
|
|
322
|
+
# @param paths [Array<String>] paths array
|
|
323
|
+
# @return [Array<String|nil, String|nil>] [src_path, dst_path]
|
|
324
|
+
#
|
|
325
|
+
def extract_paths(paths)
|
|
326
|
+
if paths.length == 2
|
|
327
|
+
[Diff.unescape_path(paths[0]), Diff.unescape_path(paths[1])]
|
|
328
|
+
else
|
|
329
|
+
path = Diff.unescape_path(paths[0])
|
|
330
|
+
[path, path]
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Build a FileRef, returning nil if the file doesn't exist on this side
|
|
335
|
+
#
|
|
336
|
+
# @param mode [String] file mode
|
|
337
|
+
# @param sha [String] file SHA
|
|
338
|
+
# @param path [String, nil] file path
|
|
339
|
+
# @return [Git::FileRef, nil]
|
|
340
|
+
#
|
|
341
|
+
def build_file_ref(mode, sha, path)
|
|
342
|
+
return nil if mode == NULL_MODE || path.nil?
|
|
343
|
+
|
|
344
|
+
Git::FileRef.new(mode: mode, sha: sha, path: path)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Parser for --patch output (combined with numstat)
|
|
349
|
+
module Patch
|
|
350
|
+
DIFF_HEADER_PATTERN = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}
|
|
351
|
+
INDEX_PATTERN = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)?/
|
|
352
|
+
FILE_MODE_PATTERN = /^(new|deleted) file mode (......)/
|
|
353
|
+
OLD_MODE_PATTERN = /^old mode (......)/
|
|
354
|
+
NEW_MODE_PATTERN = /^new mode (......)/
|
|
355
|
+
BINARY_PATTERN = /^Binary files /
|
|
356
|
+
GIT_BINARY_PATCH_PATTERN = /^GIT binary patch$/
|
|
357
|
+
RENAME_FROM_PATTERN = /^rename from (.+)$/
|
|
358
|
+
RENAME_TO_PATTERN = /^rename to (.+)$/
|
|
359
|
+
COPY_FROM_PATTERN = /^copy from (.+)$/
|
|
360
|
+
COPY_TO_PATTERN = /^copy to (.+)$/
|
|
361
|
+
SIMILARITY_PATTERN = /^similarity index (\d+)%$/
|
|
362
|
+
|
|
363
|
+
PATCH_STATUS_MAP = {
|
|
364
|
+
'new' => :added,
|
|
365
|
+
'deleted' => :deleted,
|
|
366
|
+
'modified' => :modified,
|
|
367
|
+
'renamed' => :renamed,
|
|
368
|
+
'copied' => :copied,
|
|
369
|
+
'type_changed' => :type_changed
|
|
370
|
+
}.freeze
|
|
371
|
+
|
|
372
|
+
module_function
|
|
373
|
+
|
|
374
|
+
# Parse combined patch + numstat + shortstat output into DiffResult
|
|
375
|
+
#
|
|
376
|
+
# @param output [String] combined output
|
|
377
|
+
# @param include_dirstat [Boolean] whether dirstat output is expected
|
|
378
|
+
# @return [Git::DiffResult]
|
|
379
|
+
#
|
|
380
|
+
def parse(output, include_dirstat: false)
|
|
381
|
+
return Diff.empty_result if output.empty?
|
|
382
|
+
|
|
383
|
+
numstat_lines, shortstat_line, dirstat_lines, patch_text = split_sections(output, include_dirstat)
|
|
384
|
+
numstat_map = Numstat.parse_as_map(numstat_lines)
|
|
385
|
+
|
|
386
|
+
Diff.build_result(
|
|
387
|
+
files: PatchFileParser.new(patch_text, numstat_map).parse,
|
|
388
|
+
shortstat: Diff.parse_shortstat(shortstat_line),
|
|
389
|
+
dirstat: include_dirstat ? Diff.parse_dirstat(dirstat_lines) : nil
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Split output into numstat, shortstat, dirstat, and patch sections
|
|
394
|
+
#
|
|
395
|
+
# @param output [String] combined output
|
|
396
|
+
# @param include_dirstat [Boolean] whether to expect dirstat section
|
|
397
|
+
# @return [Array<Array<String>, String|nil, Array<String>, String>]
|
|
398
|
+
#
|
|
399
|
+
def split_sections(output, include_dirstat)
|
|
400
|
+
lines = output.lines
|
|
401
|
+
first_diff_index = lines.index { |l| l.start_with?('diff --git') } || lines.length
|
|
402
|
+
pre_diff_lines = lines[0...first_diff_index].map(&:chomp).reject(&:empty?)
|
|
403
|
+
patch_text = lines[first_diff_index..].join
|
|
404
|
+
split_pre_diff(pre_diff_lines, include_dirstat, patch_text)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def split_pre_diff(pre_diff_lines, include_dirstat, patch_text)
|
|
408
|
+
shortstat_index = pre_diff_lines.index { |l| l.match?(/^\s*\d+\s+files?\s+changed/) }
|
|
409
|
+
return [pre_diff_lines, nil, [], patch_text] unless shortstat_index
|
|
410
|
+
|
|
411
|
+
[pre_diff_lines[0...shortstat_index], pre_diff_lines[shortstat_index],
|
|
412
|
+
include_dirstat ? pre_diff_lines[(shortstat_index + 1)..] : [], patch_text]
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Methods for parsing patch metadata lines (index, mode, rename, etc.)
|
|
416
|
+
# @api private
|
|
417
|
+
module PatchMetadataParser
|
|
418
|
+
private
|
|
419
|
+
|
|
420
|
+
def parse_metadata_line(line)
|
|
421
|
+
try_parse_index(line)
|
|
422
|
+
try_parse_file_mode(line)
|
|
423
|
+
try_parse_old_new_mode(line)
|
|
424
|
+
try_parse_rename(line)
|
|
425
|
+
try_parse_similarity(line)
|
|
426
|
+
try_mark_binary(line)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def try_parse_index(line)
|
|
430
|
+
return unless (match = line.match(INDEX_PATTERN))
|
|
431
|
+
|
|
432
|
+
@current_file[:src_sha] = match[1]
|
|
433
|
+
@current_file[:dst_sha] = match[2]
|
|
434
|
+
return unless (mode = match[3]&.strip)
|
|
435
|
+
return unless @current_file[:src_mode].nil? && @current_file[:dst_mode].nil?
|
|
436
|
+
|
|
437
|
+
@current_file[:src_mode] = @current_file[:dst_mode] = mode
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def try_parse_file_mode(line)
|
|
441
|
+
return unless (match = line.match(FILE_MODE_PATTERN))
|
|
442
|
+
|
|
443
|
+
type, mode = match.captures
|
|
444
|
+
@current_file[:status] = PATCH_STATUS_MAP.fetch(type, :modified)
|
|
445
|
+
apply_file_mode(type, mode)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def try_parse_old_new_mode(line)
|
|
449
|
+
if (match = line.match(OLD_MODE_PATTERN))
|
|
450
|
+
@current_file[:src_mode] = match[1]
|
|
451
|
+
detect_type_change
|
|
452
|
+
elsif (match = line.match(NEW_MODE_PATTERN))
|
|
453
|
+
@current_file[:dst_mode] = match[1]
|
|
454
|
+
detect_type_change
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def detect_type_change
|
|
459
|
+
src_mode = @current_file[:src_mode]
|
|
460
|
+
dst_mode = @current_file[:dst_mode]
|
|
461
|
+
return unless src_mode && dst_mode
|
|
462
|
+
|
|
463
|
+
# Type change occurs when the file type bits differ (e.g., 100644 vs 120000)
|
|
464
|
+
# The first 3 digits represent the file type
|
|
465
|
+
@current_file[:status] = :type_changed if src_mode[0, 3] != dst_mode[0, 3]
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def apply_file_mode(type, mode)
|
|
469
|
+
case type
|
|
470
|
+
when 'new'
|
|
471
|
+
@current_file[:dst_mode] = mode
|
|
472
|
+
@current_file[:src_path] = nil
|
|
473
|
+
when 'deleted'
|
|
474
|
+
@current_file[:src_mode] = mode
|
|
475
|
+
@current_file[:dst_path] = nil
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def try_parse_rename(line)
|
|
480
|
+
try_parse_rename_or_copy(line, RENAME_FROM_PATTERN, RENAME_TO_PATTERN, :renamed) ||
|
|
481
|
+
try_parse_rename_or_copy(line, COPY_FROM_PATTERN, COPY_TO_PATTERN, :copied)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def try_parse_rename_or_copy(line, from_pattern, to_pattern, status)
|
|
485
|
+
if (match = line.match(from_pattern))
|
|
486
|
+
@current_file[:src_path] = Diff.unescape_path(match[1])
|
|
487
|
+
@current_file[:status] = status
|
|
488
|
+
elsif (match = line.match(to_pattern))
|
|
489
|
+
@current_file[:dst_path] = Diff.unescape_path(match[1])
|
|
490
|
+
@current_file[:status] = status
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def try_parse_similarity(line)
|
|
495
|
+
return unless (match = line.match(SIMILARITY_PATTERN))
|
|
496
|
+
|
|
497
|
+
@current_file[:similarity] = match[1].to_i
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def try_mark_binary(line)
|
|
501
|
+
@current_file[:binary] = true if line.match?(BINARY_PATTERN) || line.match?(GIT_BINARY_PATCH_PATTERN)
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Stateful parser for unified diff patch output
|
|
506
|
+
# @api private
|
|
507
|
+
class PatchFileParser
|
|
508
|
+
include PatchMetadataParser
|
|
509
|
+
|
|
510
|
+
def initialize(patch_text, numstat_map = {})
|
|
511
|
+
@patch_text = patch_text
|
|
512
|
+
@numstat_map = numstat_map
|
|
513
|
+
@files = []
|
|
514
|
+
@current_file = nil
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def parse
|
|
518
|
+
@patch_text.split("\n").each { |line| process_line(line) }
|
|
519
|
+
finalize_current_file
|
|
520
|
+
@files
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
private
|
|
524
|
+
|
|
525
|
+
def process_line(line)
|
|
526
|
+
if (match = line.match(DIFF_HEADER_PATTERN))
|
|
527
|
+
start_new_file(match, line)
|
|
528
|
+
elsif @current_file
|
|
529
|
+
append_to_current_file(line)
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def start_new_file(match, line)
|
|
534
|
+
finalize_current_file
|
|
535
|
+
@current_file = default_file_state.merge(
|
|
536
|
+
patch: line,
|
|
537
|
+
src_path: Git::EscapedPath.new(match[2]).unescape,
|
|
538
|
+
dst_path: Git::EscapedPath.new(match[4]).unescape
|
|
539
|
+
)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def default_file_state
|
|
543
|
+
{ src_mode: nil, dst_mode: nil, src_sha: '', dst_sha: '',
|
|
544
|
+
src_path: nil, dst_path: nil, status: :modified, similarity: nil, binary: false }
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def append_to_current_file(line)
|
|
548
|
+
parse_metadata_line(line)
|
|
549
|
+
@current_file[:patch] = "#{@current_file[:patch]}\n#{line}"
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def finalize_current_file
|
|
553
|
+
return unless @current_file
|
|
554
|
+
|
|
555
|
+
@files << build_patch_info
|
|
556
|
+
@current_file = nil
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def build_patch_info
|
|
560
|
+
path = @current_file[:dst_path] || @current_file[:src_path]
|
|
561
|
+
stats = @numstat_map.fetch(path, { insertions: 0, deletions: 0 })
|
|
562
|
+
|
|
563
|
+
Git::DiffFilePatchInfo.new(
|
|
564
|
+
src: build_file_ref(:src), dst: build_file_ref(:dst),
|
|
565
|
+
patch: @current_file[:patch], status: @current_file[:status],
|
|
566
|
+
similarity: @current_file[:similarity], binary: @current_file[:binary],
|
|
567
|
+
insertions: stats[:insertions], deletions: stats[:deletions]
|
|
568
|
+
)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def build_file_ref(side)
|
|
572
|
+
path = @current_file[:"#{side}_path"]
|
|
573
|
+
return nil if path.nil?
|
|
574
|
+
|
|
575
|
+
Git::FileRef.new(
|
|
576
|
+
mode: @current_file[:"#{side}_mode"] || '',
|
|
577
|
+
sha: @current_file[:"#{side}_sha"] || '',
|
|
578
|
+
path: path
|
|
579
|
+
)
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
end
|