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,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'git/fsck_object'
|
|
4
|
+
require 'git/fsck_result'
|
|
5
|
+
|
|
6
|
+
module Git
|
|
7
|
+
module Parsers
|
|
8
|
+
# Parser for git fsck command output
|
|
9
|
+
#
|
|
10
|
+
# Handles parsing of `git fsck` output into structured data objects
|
|
11
|
+
# for dangling, missing, unreachable objects, warnings, roots, and tagged objects.
|
|
12
|
+
#
|
|
13
|
+
# ## Design Note: Namespace Organization
|
|
14
|
+
#
|
|
15
|
+
# This parser creates and returns {Git::FsckObject} and {Git::FsckResult}
|
|
16
|
+
# objects, which live at the top-level `Git::` namespace rather than within
|
|
17
|
+
# `Git::Parsers::`. This is intentional:
|
|
18
|
+
#
|
|
19
|
+
# - **Parsers are infrastructure** - marked `@api private`, users shouldn't
|
|
20
|
+
# interact with them directly
|
|
21
|
+
# - **Result classes are public API** - returned by commands and used
|
|
22
|
+
# throughout the codebase
|
|
23
|
+
# - **FsckObject and FsckResult represent operation outcomes** - they describe
|
|
24
|
+
# repository integrity status, not parsing details
|
|
25
|
+
#
|
|
26
|
+
# Keeping these classes at `Git::` improves discoverability and correctly
|
|
27
|
+
# reflects their role as public types rather than parser internals.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
#
|
|
31
|
+
module Fsck
|
|
32
|
+
# Pattern matcher for dangling/missing/unreachable object lines
|
|
33
|
+
# Matches lines like:
|
|
34
|
+
# dangling commit abc123...
|
|
35
|
+
# missing blob def456...
|
|
36
|
+
# unreachable tree 789abc... (name)
|
|
37
|
+
OBJECT_PATTERN = /\A(dangling|missing|unreachable) (\w+) ([0-9a-f]{40})(?: \((.+)\))?\z/
|
|
38
|
+
|
|
39
|
+
# Pattern matcher for warning lines
|
|
40
|
+
# Matches lines like:
|
|
41
|
+
# warning in commit abc123...: message here
|
|
42
|
+
WARNING_PATTERN = /\Awarning in (\w+) ([0-9a-f]{40}): (.+)\z/
|
|
43
|
+
|
|
44
|
+
# Pattern matcher for root commit lines
|
|
45
|
+
# Matches lines like:
|
|
46
|
+
# root abc123...
|
|
47
|
+
ROOT_PATTERN = /\Aroot ([0-9a-f]{40})\z/
|
|
48
|
+
|
|
49
|
+
# Pattern matcher for tagged object lines
|
|
50
|
+
# Matches lines like:
|
|
51
|
+
# tagged commit abc123... (tagname) in def456...
|
|
52
|
+
TAGGED_PATTERN = /\Atagged (\w+) ([0-9a-f]{40}) \((.+)\) in ([0-9a-f]{40})\z/
|
|
53
|
+
|
|
54
|
+
module_function
|
|
55
|
+
|
|
56
|
+
# Parse git fsck output into a FsckResult object
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# FsckParser.parse("dangling commit abc123...\nmissing blob def456...\n")
|
|
60
|
+
# # => #<Git::FsckResult dangling: [...], missing: [...]>
|
|
61
|
+
#
|
|
62
|
+
# @param stdout [String] output from git fsck command
|
|
63
|
+
# @return [Git::FsckResult] the parsed result
|
|
64
|
+
#
|
|
65
|
+
def parse(stdout)
|
|
66
|
+
result = { dangling: [], missing: [], unreachable: [], warnings: [], root: [], tagged: [] }
|
|
67
|
+
stdout.each_line { |line| parse_line(line.strip, result) }
|
|
68
|
+
Git::FsckResult.new(**result)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Parse a single line of fsck output
|
|
72
|
+
#
|
|
73
|
+
# @param line [String] a line of output
|
|
74
|
+
# @param result [Hash] the result hash to populate
|
|
75
|
+
# @return [Boolean] true if the line was parsed
|
|
76
|
+
#
|
|
77
|
+
def parse_line(line, result)
|
|
78
|
+
parse_object_line(line, result) ||
|
|
79
|
+
parse_warning_line(line, result) ||
|
|
80
|
+
parse_root_line(line, result) ||
|
|
81
|
+
parse_tagged_line(line, result)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Parse a dangling/missing/unreachable object line
|
|
85
|
+
#
|
|
86
|
+
# @param line [String] a line of output
|
|
87
|
+
# @param result [Hash] the result hash to populate
|
|
88
|
+
# @return [Boolean] true if the line was parsed
|
|
89
|
+
#
|
|
90
|
+
def parse_object_line(line, result)
|
|
91
|
+
return unless (match = OBJECT_PATTERN.match(line))
|
|
92
|
+
|
|
93
|
+
result[match[1].to_sym] << Git::FsckObject.new(type: match[2].to_sym, oid: match[3], name: match[4])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Parse a warning line
|
|
97
|
+
#
|
|
98
|
+
# @param line [String] a line of output
|
|
99
|
+
# @param result [Hash] the result hash to populate
|
|
100
|
+
# @return [Boolean] true if the line was parsed
|
|
101
|
+
#
|
|
102
|
+
def parse_warning_line(line, result)
|
|
103
|
+
return unless (match = WARNING_PATTERN.match(line))
|
|
104
|
+
|
|
105
|
+
result[:warnings] << Git::FsckObject.new(type: match[1].to_sym, oid: match[2], message: match[3])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Parse a root line
|
|
109
|
+
#
|
|
110
|
+
# @param line [String] a line of output
|
|
111
|
+
# @param result [Hash] the result hash to populate
|
|
112
|
+
# @return [Boolean] true if the line was parsed
|
|
113
|
+
#
|
|
114
|
+
def parse_root_line(line, result)
|
|
115
|
+
return unless (match = ROOT_PATTERN.match(line))
|
|
116
|
+
|
|
117
|
+
result[:root] << Git::FsckObject.new(type: :commit, oid: match[1])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Parse a tagged line
|
|
121
|
+
#
|
|
122
|
+
# @param line [String] a line of output
|
|
123
|
+
# @param result [Hash] the result hash to populate
|
|
124
|
+
# @return [Boolean] true if the line was parsed
|
|
125
|
+
#
|
|
126
|
+
def parse_tagged_line(line, result)
|
|
127
|
+
return unless (match = TAGGED_PATTERN.match(line))
|
|
128
|
+
|
|
129
|
+
result[:tagged] << Git::FsckObject.new(type: match[1].to_sym, oid: match[2], name: match[3])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Parsers
|
|
5
|
+
# Parser for `git grep` output
|
|
6
|
+
#
|
|
7
|
+
# Provides a class method that transforms raw `git grep --null` output into a
|
|
8
|
+
# structured Hash consumed by the `Git::Repository::ObjectOperations` facade.
|
|
9
|
+
#
|
|
10
|
+
# This parser is a pure text transformer with no exit-status logic. The
|
|
11
|
+
# calling facade is responsible for interpreting the command's exit status
|
|
12
|
+
# before delegating output parsing to this class.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
#
|
|
16
|
+
module Grep
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Parse `git grep --line-number --null --no-color` output into a match hash
|
|
20
|
+
#
|
|
21
|
+
# With `--null`, git separates the path and line number fields with NUL
|
|
22
|
+
# bytes: `treeish:filename\0linenum\0text\n`. This keeps filenames that
|
|
23
|
+
# contain `:<digits>:` from being confused with the line-number delimiter.
|
|
24
|
+
#
|
|
25
|
+
# @param output [String] raw output from `git grep --null --line-number`
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash<String, Array<Array(Integer, String)>>] hash mapping
|
|
28
|
+
# `"treeish:filename"` keys to arrays of `[line_number, text]` pairs
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
#
|
|
32
|
+
def parse(output)
|
|
33
|
+
output.each_line.with_object(Hash.new { |h, k| h[k] = [] }) do |line, hsh|
|
|
34
|
+
filename, line_num, text = line.chomp.split("\0", 3)
|
|
35
|
+
next unless text && line_num&.match?(/\A\d+\z/)
|
|
36
|
+
|
|
37
|
+
hsh[filename] << [line_num.to_i, text]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'git/escaped_path'
|
|
4
|
+
|
|
5
|
+
module Git
|
|
6
|
+
module Parsers
|
|
7
|
+
# Parser for `git ls-tree` output
|
|
8
|
+
#
|
|
9
|
+
# Provides a class method that transforms raw `git ls-tree` output into a
|
|
10
|
+
# structured Hash consumed by the `Git::Repository::ObjectOperations` facade.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
#
|
|
14
|
+
module LsTree
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Parse `git ls-tree` output into a type-keyed hash of entries
|
|
18
|
+
#
|
|
19
|
+
# Each line of output is expected in the format produced by
|
|
20
|
+
# `git ls-tree`: `<mode> <type> <sha>\t<file>`.
|
|
21
|
+
#
|
|
22
|
+
# @param output [String] raw stdout from `git ls-tree`
|
|
23
|
+
#
|
|
24
|
+
# @return [Hash<String, Hash<String, Hash>>] hash keyed by object type
|
|
25
|
+
# (`'blob'`, `'tree'`, `'commit'`), then by filename, holding
|
|
26
|
+
# `:mode` and `:sha` values
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
29
|
+
#
|
|
30
|
+
def parse(output)
|
|
31
|
+
data = { 'blob' => {}, 'tree' => {}, 'commit' => {} }
|
|
32
|
+
output.split("\n").each do |line|
|
|
33
|
+
info, filenm = line.split("\t", 2)
|
|
34
|
+
filenm = unescape_path(filenm) if filenm
|
|
35
|
+
mode, type, entry_sha = info.split
|
|
36
|
+
data[type][filenm] = { mode: mode, sha: entry_sha }
|
|
37
|
+
end
|
|
38
|
+
data
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Converts a git-quoted path back to its original form
|
|
42
|
+
#
|
|
43
|
+
# @param path [String] the path, possibly git-quoted
|
|
44
|
+
#
|
|
45
|
+
# @return [String] the unquoted path
|
|
46
|
+
#
|
|
47
|
+
# @api private
|
|
48
|
+
#
|
|
49
|
+
def unescape_path(path)
|
|
50
|
+
if path.start_with?('"') && path.end_with?('"')
|
|
51
|
+
Git::EscapedPath.new(path[1..-2]).unescape
|
|
52
|
+
else
|
|
53
|
+
path
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'git/stash_info'
|
|
4
|
+
|
|
5
|
+
module Git
|
|
6
|
+
module Parsers
|
|
7
|
+
# Parser for git stash command output
|
|
8
|
+
#
|
|
9
|
+
# Handles parsing of `git stash list` output into structured data objects.
|
|
10
|
+
#
|
|
11
|
+
# @note Known limitation: If a stash message contains the field separator
|
|
12
|
+
# character (\x1f, ASCII unit separator), parsing will fail or produce
|
|
13
|
+
# incorrect results. This is extremely rare in practice since \x1f is a
|
|
14
|
+
# non-printable control character.
|
|
15
|
+
#
|
|
16
|
+
# ## Design Note: Namespace Organization
|
|
17
|
+
#
|
|
18
|
+
# This parser creates and returns {Git::StashInfo} objects, which live at
|
|
19
|
+
# the top-level `Git::` namespace rather than within `Git::Parsers::`. This
|
|
20
|
+
# is intentional:
|
|
21
|
+
#
|
|
22
|
+
# - **Parsers are infrastructure** - marked `@api private`, users shouldn't
|
|
23
|
+
# interact with them directly
|
|
24
|
+
# - **Info classes are public API** - returned by commands and used throughout
|
|
25
|
+
# the codebase
|
|
26
|
+
# - **Info classes are domain entities** - represent core git concepts
|
|
27
|
+
# (stashes as data)
|
|
28
|
+
#
|
|
29
|
+
# Keeping Info classes at `Git::` improves discoverability and correctly
|
|
30
|
+
# reflects their role as public types rather than parser internals.
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
33
|
+
#
|
|
34
|
+
module Stash
|
|
35
|
+
# Field separator used in custom format output
|
|
36
|
+
# Using a non-printable unit separator (US, 0x1F) to avoid collisions with
|
|
37
|
+
# stash messages and author/committer fields, while still working with
|
|
38
|
+
# Process.spawn (which doesn't allow NUL bytes in arguments)
|
|
39
|
+
FIELD_SEPARATOR = "\x1f"
|
|
40
|
+
|
|
41
|
+
# Custom format for git stash list that extracts all available metadata
|
|
42
|
+
# %H = full commit SHA
|
|
43
|
+
# %h = abbreviated commit SHA
|
|
44
|
+
# %gd = reflog selector (stash@\\{n})
|
|
45
|
+
# %gs = reflog subject (the stash message)
|
|
46
|
+
# %an = author name
|
|
47
|
+
# %ae = author email
|
|
48
|
+
# %aI = author date (ISO 8601 format)
|
|
49
|
+
# %cn = committer name
|
|
50
|
+
# %ce = committer email
|
|
51
|
+
# %cI = committer date (ISO 8601 format)
|
|
52
|
+
STASH_FORMAT = [
|
|
53
|
+
'%H', # 0: full SHA
|
|
54
|
+
'%h', # 1: short SHA
|
|
55
|
+
'%gd', # 2: reflog selector
|
|
56
|
+
'%gs', # 3: reflog subject (message)
|
|
57
|
+
'%an', # 4: author name
|
|
58
|
+
'%ae', # 5: author email
|
|
59
|
+
'%aI', # 6: author date
|
|
60
|
+
'%cn', # 7: committer name
|
|
61
|
+
'%ce', # 8: committer email
|
|
62
|
+
'%cI' # 9: committer date
|
|
63
|
+
].join(FIELD_SEPARATOR)
|
|
64
|
+
|
|
65
|
+
# Number of fields expected in the parsed output
|
|
66
|
+
FIELD_COUNT = 10
|
|
67
|
+
|
|
68
|
+
# Field indices for parsed output
|
|
69
|
+
module Fields
|
|
70
|
+
OID = 0
|
|
71
|
+
SHORT_OID = 1
|
|
72
|
+
REFLOG = 2
|
|
73
|
+
MESSAGE = 3
|
|
74
|
+
AUTHOR_NAME = 4
|
|
75
|
+
AUTHOR_EMAIL = 5
|
|
76
|
+
AUTHOR_DATE = 6
|
|
77
|
+
COMMITTER_NAME = 7
|
|
78
|
+
COMMITTER_EMAIL = 8
|
|
79
|
+
COMMITTER_DATE = 9
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Pattern to extract branch from standard stash messages
|
|
83
|
+
# Matches "WIP on <branch>:" or "On <branch>:" at the start
|
|
84
|
+
BRANCH_PATTERN = /^(?:WIP on|On)\s+([^:]+):/
|
|
85
|
+
|
|
86
|
+
module_function
|
|
87
|
+
|
|
88
|
+
# Parse git stash list output into StashInfo objects
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# StashParser.parse_list("abc123\x1fabc\x1fstash@\\{0}\x1fWIP on main: msg\x1f...\n")
|
|
92
|
+
# # => [#<Git::StashInfo index: 0, ...>]
|
|
93
|
+
#
|
|
94
|
+
# @param stdout [String] output from git stash list --format=...
|
|
95
|
+
# @return [Array<Git::StashInfo>] parsed stash information
|
|
96
|
+
#
|
|
97
|
+
# @raise [Git::UnexpectedResultError] if stash output cannot be parsed
|
|
98
|
+
#
|
|
99
|
+
def parse_list(stdout)
|
|
100
|
+
lines = stdout.split("\n")
|
|
101
|
+
lines.each_with_index.map { |line, idx| parse_stash_line(line, idx, lines) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Parse a single stash list line into a StashInfo object
|
|
105
|
+
#
|
|
106
|
+
# @param line [String] a line from git stash list output (custom format)
|
|
107
|
+
# @param expected_index [Integer] the expected stash index for validation
|
|
108
|
+
# @param all_lines [Array<String>] all output lines (for error messages)
|
|
109
|
+
#
|
|
110
|
+
# @return [Git::StashInfo] parsed stash info
|
|
111
|
+
#
|
|
112
|
+
# @raise [Git::UnexpectedResultError] if line format is unexpected
|
|
113
|
+
#
|
|
114
|
+
def parse_stash_line(line, expected_index, all_lines)
|
|
115
|
+
parts = line.split(FIELD_SEPARATOR, FIELD_COUNT)
|
|
116
|
+
return build_stash_info(parts, expected_index) if parts.length == FIELD_COUNT
|
|
117
|
+
|
|
118
|
+
raise Git::UnexpectedResultError, unexpected_stash_line_error(all_lines, line, expected_index)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build a StashInfo from parsed format parts
|
|
122
|
+
#
|
|
123
|
+
# @param parts [Array<String>] the parsed format fields
|
|
124
|
+
# @param expected_index [Integer] fallback index if not parseable from reflog
|
|
125
|
+
# @return [Git::StashInfo]
|
|
126
|
+
#
|
|
127
|
+
def build_stash_info(parts, expected_index)
|
|
128
|
+
index = extract_index(parts[Fields::REFLOG]) || expected_index
|
|
129
|
+
|
|
130
|
+
Git::StashInfo.new(**stash_info_attrs(parts, index))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Build StashInfo attributes hash from parsed parts
|
|
134
|
+
#
|
|
135
|
+
# @param parts [Array<String>] the parsed format fields
|
|
136
|
+
# @param index [Integer] the resolved stash index
|
|
137
|
+
# @return [Hash] attributes for StashInfo.new
|
|
138
|
+
#
|
|
139
|
+
def stash_info_attrs(parts, index)
|
|
140
|
+
core_attrs(parts, index).merge(author_attrs(parts)).merge(committer_attrs(parts))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def core_attrs(parts, index)
|
|
144
|
+
{
|
|
145
|
+
index: index, name: parts[Fields::REFLOG], oid: parts[Fields::OID],
|
|
146
|
+
short_oid: parts[Fields::SHORT_OID], branch: extract_branch(parts[Fields::MESSAGE]),
|
|
147
|
+
message: parts[Fields::MESSAGE]
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def author_attrs(parts)
|
|
152
|
+
{
|
|
153
|
+
author_name: parts[Fields::AUTHOR_NAME], author_email: parts[Fields::AUTHOR_EMAIL],
|
|
154
|
+
author_date: parts[Fields::AUTHOR_DATE]
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def committer_attrs(parts)
|
|
159
|
+
{
|
|
160
|
+
committer_name: parts[Fields::COMMITTER_NAME], committer_email: parts[Fields::COMMITTER_EMAIL],
|
|
161
|
+
committer_date: parts[Fields::COMMITTER_DATE]
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Extract the stash index from a reflog selector
|
|
166
|
+
#
|
|
167
|
+
# @param reflog_selector [String] e.g., "stash@\\{0}"
|
|
168
|
+
# @return [Integer, nil] the index or nil if not found
|
|
169
|
+
#
|
|
170
|
+
def extract_index(reflog_selector)
|
|
171
|
+
match = reflog_selector&.match(/stash@\{(\d+)\}/)
|
|
172
|
+
match ? match[1].to_i : nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Extract the branch name from a stash message
|
|
176
|
+
#
|
|
177
|
+
# @param message [String] the stash message
|
|
178
|
+
# @return [String, nil] the branch name or nil for custom messages
|
|
179
|
+
#
|
|
180
|
+
def extract_branch(message)
|
|
181
|
+
match = BRANCH_PATTERN.match(message)
|
|
182
|
+
match ? match[1] : nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Generate error message for unexpected stash line format
|
|
186
|
+
#
|
|
187
|
+
# @param lines [Array<String>] all output lines
|
|
188
|
+
# @param line [String] the problematic line
|
|
189
|
+
# @param index [Integer] the stash index
|
|
190
|
+
# @return [String] formatted error message
|
|
191
|
+
#
|
|
192
|
+
def unexpected_stash_line_error(lines, line, index)
|
|
193
|
+
format_str = STASH_FORMAT.gsub(FIELD_SEPARATOR, '<FS>')
|
|
194
|
+
<<~ERROR
|
|
195
|
+
Unexpected line in output from `git stash list --format=#{format_str}`, at index #{index}
|
|
196
|
+
|
|
197
|
+
Expected #{FIELD_COUNT} fields separated by '\\x1f' (unit separator), got #{line.split(FIELD_SEPARATOR, -1).length}
|
|
198
|
+
|
|
199
|
+
Full output:
|
|
200
|
+
#{lines.join("\n ")}
|
|
201
|
+
|
|
202
|
+
Line at index #{index}:
|
|
203
|
+
"#{line}"
|
|
204
|
+
ERROR
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'git/tag_info'
|
|
4
|
+
require 'git/tag_delete_result'
|
|
5
|
+
require 'git/tag_delete_failure'
|
|
6
|
+
|
|
7
|
+
module Git
|
|
8
|
+
module Parsers
|
|
9
|
+
# Parser for git tag command output
|
|
10
|
+
#
|
|
11
|
+
# Handles parsing of `git tag --list` and `git tag --delete` output
|
|
12
|
+
# into structured data objects.
|
|
13
|
+
#
|
|
14
|
+
# @note Known limitation: If a tag message contains the field delimiter
|
|
15
|
+
# character (\x1f, ASCII unit separator), it will be preserved correctly
|
|
16
|
+
# since the message is the last field. However, messages are rarely crafted
|
|
17
|
+
# with non-printable control characters.
|
|
18
|
+
#
|
|
19
|
+
# ## Design Note: Namespace Organization
|
|
20
|
+
#
|
|
21
|
+
# This parser creates and returns {Git::TagInfo} and {Git::TagDeleteResult}
|
|
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 core git concepts
|
|
30
|
+
# (tags as data)
|
|
31
|
+
# - **Result classes are operation outcomes** - represent command results,
|
|
32
|
+
# not parsing details
|
|
33
|
+
#
|
|
34
|
+
# Keeping Info/Result classes at `Git::` improves discoverability and correctly
|
|
35
|
+
# reflects their role as public types rather than parser internals.
|
|
36
|
+
#
|
|
37
|
+
# @api private
|
|
38
|
+
#
|
|
39
|
+
module Tag
|
|
40
|
+
# Delimiter for separating fields in git tag --format output
|
|
41
|
+
# Field separator used in custom format output
|
|
42
|
+
# Using the ASCII unit separator (US, 0x1F / "\x1f"), a non-printable character,
|
|
43
|
+
# minimizes the chance of collisions with tag names or messages and remains
|
|
44
|
+
# safe to pass through Process.spawn and shell argument boundaries.
|
|
45
|
+
FIELD_DELIMITER = "\x1f"
|
|
46
|
+
|
|
47
|
+
# Delimiter for separating records (tags) in output
|
|
48
|
+
# Using the ASCII record separator (RS, 0x1E / "\x1e") to delimit complete tag records.
|
|
49
|
+
# This allows multi-line messages (which contain newlines) to be parsed correctly
|
|
50
|
+
# since we split by record separator first, then by field delimiter.
|
|
51
|
+
RECORD_DELIMITER = "\x1e"
|
|
52
|
+
|
|
53
|
+
# Number of fields expected in the parsed output
|
|
54
|
+
FIELD_COUNT = 8
|
|
55
|
+
|
|
56
|
+
# Format string for git tag --format
|
|
57
|
+
#
|
|
58
|
+
# Fields:
|
|
59
|
+
# - %(refname:short) - tag name
|
|
60
|
+
# - %(objectname) - SHA of the tag object (for annotated) or commit (for lightweight)
|
|
61
|
+
# - %(*objectname) - Dereferenced SHA (commit ID for annotated tags, empty for lightweight)
|
|
62
|
+
# - %(objecttype) - 'tag' for annotated tags, target object type (commit/tree/blob/etc.) for lightweight tags
|
|
63
|
+
# - %(taggername) - tagger name (empty for lightweight tags)
|
|
64
|
+
# - %(taggeremail) - tagger email (empty for lightweight tags)
|
|
65
|
+
# - %(taggerdate:iso8601-strict) - tagger date in strict ISO 8601 format
|
|
66
|
+
# - %(contents) - full tag message (can be multi-line)
|
|
67
|
+
#
|
|
68
|
+
# Each tag record is terminated by the RECORD_DELIMITER to allow multi-line messages.
|
|
69
|
+
FORMAT_STRING = [
|
|
70
|
+
'%(refname:short)',
|
|
71
|
+
'%(objectname)',
|
|
72
|
+
'%(*objectname)',
|
|
73
|
+
'%(objecttype)',
|
|
74
|
+
'%(taggername)',
|
|
75
|
+
'%(taggeremail)',
|
|
76
|
+
'%(taggerdate:iso8601-strict)',
|
|
77
|
+
'%(contents)'
|
|
78
|
+
].join(FIELD_DELIMITER) + RECORD_DELIMITER
|
|
79
|
+
|
|
80
|
+
# Regex to parse successful deletion lines from stdout
|
|
81
|
+
# Matches: Deleted tag 'tagname' (was abc123)
|
|
82
|
+
DELETED_TAG_REGEX = /^Deleted tag '([^']+)'/
|
|
83
|
+
|
|
84
|
+
# Regex to parse error messages from stderr
|
|
85
|
+
# Matches: error: tag 'tagname' not found.
|
|
86
|
+
ERROR_TAG_REGEX = /^error: tag '([^']+)'(.*)$/
|
|
87
|
+
|
|
88
|
+
module_function
|
|
89
|
+
|
|
90
|
+
# Parse git tag --list output into TagInfo objects
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# TagParser.parse_list("v1.0.0\x1f...\x1e\n")
|
|
94
|
+
# # => [#<Git::TagInfo name: "v1.0.0", ...>]
|
|
95
|
+
#
|
|
96
|
+
# @param stdout [String] output from git tag --list --format=...
|
|
97
|
+
# @return [Array<Git::TagInfo>] parsed tag information
|
|
98
|
+
#
|
|
99
|
+
# @raise [Git::UnexpectedResultError] if any record has unexpected format
|
|
100
|
+
#
|
|
101
|
+
def parse_list(stdout)
|
|
102
|
+
# Split by record separator
|
|
103
|
+
# Each record may have a leading newline from the previous record's %(contents) output
|
|
104
|
+
# Use lstrip to remove leading whitespace (which includes the newline) from each record
|
|
105
|
+
records = stdout.split(RECORD_DELIMITER).map(&:lstrip).reject(&:empty?)
|
|
106
|
+
records.map.with_index { |record, index| parse_tag_record(record, index, records) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Parse a single formatted tag record
|
|
110
|
+
#
|
|
111
|
+
# The record format is:
|
|
112
|
+
# name<FS>sha<FS>deref<FS>objecttype<FS>tagger_name<FS>tagger_email<FS>tagger_date<FS>message
|
|
113
|
+
# where <FS> is the unit separator character ("\x1f").
|
|
114
|
+
#
|
|
115
|
+
# For lightweight tags, Git emits empty strings for the tagger fields and message;
|
|
116
|
+
# these are converted to nil by {#parse_optional_field} and {#parse_message}.
|
|
117
|
+
#
|
|
118
|
+
# @param record [String] a single tag record from git tag --format output
|
|
119
|
+
# @param index [Integer] record index for error reporting
|
|
120
|
+
# @param all_records [Array<String>] all output records for error messages
|
|
121
|
+
# @return [Git::TagInfo] tag info with all fields populated
|
|
122
|
+
#
|
|
123
|
+
# @raise [Git::UnexpectedResultError] if record format is unexpected
|
|
124
|
+
#
|
|
125
|
+
def parse_tag_record(record, index, all_records)
|
|
126
|
+
parts = record.split(FIELD_DELIMITER, FIELD_COUNT)
|
|
127
|
+
|
|
128
|
+
unless parts.length == FIELD_COUNT
|
|
129
|
+
raise Git::UnexpectedResultError, unexpected_tag_record_error(all_records, record, index)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
build_tag_info(parts)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Build a TagInfo object from parsed parts
|
|
136
|
+
#
|
|
137
|
+
# @param parts [Array<String>] the parsed format fields
|
|
138
|
+
# @return [Git::TagInfo]
|
|
139
|
+
#
|
|
140
|
+
# @note For annotated tags:
|
|
141
|
+
# - oid = %(objectname) (the tag object's ID)
|
|
142
|
+
# - target_oid = %(*objectname) (the dereferenced commit ID)
|
|
143
|
+
#
|
|
144
|
+
# @note For lightweight tags:
|
|
145
|
+
# - oid = nil (lightweight tags are not objects)
|
|
146
|
+
# - target_oid = %(objectname) (the commit ID)
|
|
147
|
+
#
|
|
148
|
+
def build_tag_info(parts)
|
|
149
|
+
oid, target_oid = resolve_oids(parts[3], parts[1], parts[2])
|
|
150
|
+
build_tag_info_object(parts, oid, target_oid)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def resolve_oids(objecttype, objectname, dereferenced)
|
|
154
|
+
objecttype == 'tag' ? [objectname, dereferenced] : [nil, objectname]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def build_tag_info_object(parts, oid, target_oid)
|
|
158
|
+
Git::TagInfo.new(
|
|
159
|
+
name: parts[0], oid: oid, target_oid: target_oid, objecttype: parts[3],
|
|
160
|
+
tagger_name: parse_optional_field(parts[4]), tagger_email: parse_optional_field(parts[5]),
|
|
161
|
+
tagger_date: parse_optional_field(parts[6]), message: parse_message(parts[3], parts[7])
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Parse an optional field, returning nil if empty
|
|
166
|
+
#
|
|
167
|
+
# @param value [String] the field value
|
|
168
|
+
# @return [String, nil] the value or nil if empty
|
|
169
|
+
#
|
|
170
|
+
def parse_optional_field(value)
|
|
171
|
+
value.empty? ? nil : value
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Parse message field, returning nil for lightweight tags or empty messages
|
|
175
|
+
# Strips trailing newlines that git adds to %(contents) output
|
|
176
|
+
#
|
|
177
|
+
# @param objecttype [String] the object type ('tag' or 'commit')
|
|
178
|
+
# @param message [String] the raw message field
|
|
179
|
+
# @return [String, nil] the message or nil
|
|
180
|
+
#
|
|
181
|
+
def parse_message(objecttype, message)
|
|
182
|
+
stripped = message.chomp
|
|
183
|
+
objecttype == 'tag' && !stripped.empty? ? stripped : nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Parse deleted tag names from stdout
|
|
187
|
+
#
|
|
188
|
+
# @example
|
|
189
|
+
# TagParser.parse_deleted_tags("Deleted tag 'v1.0.0' (was abc123)\n")
|
|
190
|
+
# # => ["v1.0.0"]
|
|
191
|
+
#
|
|
192
|
+
# @param stdout [String] command stdout
|
|
193
|
+
# @return [Array<String>] names of successfully deleted tags
|
|
194
|
+
#
|
|
195
|
+
def parse_deleted_tags(stdout)
|
|
196
|
+
stdout.scan(DELETED_TAG_REGEX).flatten
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Parse error messages from stderr into a map
|
|
200
|
+
#
|
|
201
|
+
# @example
|
|
202
|
+
# TagParser.parse_error_messages("error: tag 'missing' not found.\n")
|
|
203
|
+
# # => {"missing" => "error: tag 'missing' not found."}
|
|
204
|
+
#
|
|
205
|
+
# @param stderr [String] command stderr
|
|
206
|
+
# @return [Hash<String, String>] map of tag name to error message
|
|
207
|
+
#
|
|
208
|
+
def parse_error_messages(stderr)
|
|
209
|
+
stderr.each_line.with_object({}) do |line, hash|
|
|
210
|
+
match = line.match(ERROR_TAG_REGEX)
|
|
211
|
+
hash[match[1]] = line.strip if match
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Build the TagDeleteResult from parsed data
|
|
216
|
+
#
|
|
217
|
+
# @param requested_names [Array<String>] originally requested tag names
|
|
218
|
+
# @param existing_tags [Hash<String, Git::TagInfo>] tags that existed before delete
|
|
219
|
+
# @param deleted_names [Array<String>] names confirmed deleted in stdout
|
|
220
|
+
# @param error_map [Hash<String, String>] map of tag name to error message
|
|
221
|
+
# @return [Git::TagDeleteResult] the result object
|
|
222
|
+
#
|
|
223
|
+
def build_delete_result(requested_names, existing_tags, deleted_names, error_map)
|
|
224
|
+
deleted = deleted_names.filter_map { |name| existing_tags[name] }
|
|
225
|
+
|
|
226
|
+
not_deleted = (requested_names - deleted_names).map do |name|
|
|
227
|
+
error_message = error_map[name] || "tag '#{name}' could not be deleted"
|
|
228
|
+
Git::TagDeleteFailure.new(name: name, error_message: error_message)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
Git::TagDeleteResult.new(deleted: deleted, not_deleted: not_deleted)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Generate error message for unexpected tag record format
|
|
235
|
+
#
|
|
236
|
+
# @param records [Array<String>] all output records
|
|
237
|
+
# @param record [String] the problematic record
|
|
238
|
+
# @param index [Integer] the record index
|
|
239
|
+
# @return [String] formatted error message
|
|
240
|
+
#
|
|
241
|
+
def unexpected_tag_record_error(records, record, index)
|
|
242
|
+
format_str = FORMAT_STRING.gsub(FIELD_DELIMITER, '<FS>').gsub(RECORD_DELIMITER, '<RS>')
|
|
243
|
+
<<~ERROR
|
|
244
|
+
Unexpected record in output from `git tag --list --format=#{format_str}`, at index #{index}
|
|
245
|
+
|
|
246
|
+
Expected #{FIELD_COUNT} fields separated by '\\x1f' (unit separator), got #{record.split(FIELD_DELIMITER, -1).length}
|
|
247
|
+
|
|
248
|
+
Full output:
|
|
249
|
+
#{records.join("\n ")}
|
|
250
|
+
|
|
251
|
+
Record at index #{index}:
|
|
252
|
+
"#{record}"
|
|
253
|
+
ERROR
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|