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,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
require 'git/author_info'
|
|
6
|
+
require 'git/stash_info'
|
|
7
|
+
|
|
8
|
+
module Git
|
|
9
|
+
module Parsers
|
|
10
|
+
# Parser for git stash command output
|
|
11
|
+
#
|
|
12
|
+
# Handles parsing of `git stash list` output into structured data objects.
|
|
13
|
+
#
|
|
14
|
+
# @note Known limitation: If a stash message contains the field separator
|
|
15
|
+
# character (\x1f, ASCII unit separator), parsing will fail or produce
|
|
16
|
+
# incorrect results. This is extremely rare in practice since \x1f is a
|
|
17
|
+
# non-printable control character.
|
|
18
|
+
#
|
|
19
|
+
# ## Design Note: Namespace Organization
|
|
20
|
+
#
|
|
21
|
+
# This parser creates and returns {Git::StashInfo} objects, which live at
|
|
22
|
+
# the top-level `Git::` namespace rather than within `Git::Parsers::`. This
|
|
23
|
+
# is intentional:
|
|
24
|
+
#
|
|
25
|
+
# - **Parsers are infrastructure** - marked `@api private`, users shouldn't
|
|
26
|
+
# interact with them directly
|
|
27
|
+
# - **Info classes are public API** - returned by commands and used throughout
|
|
28
|
+
# the codebase
|
|
29
|
+
# - **Info classes are domain entities** - represent core git concepts
|
|
30
|
+
# (stashes as data)
|
|
31
|
+
#
|
|
32
|
+
# Keeping Info classes at `Git::` improves discoverability and correctly
|
|
33
|
+
# reflects their role as public types rather than parser internals.
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
#
|
|
37
|
+
module Stash
|
|
38
|
+
# Field separator used in custom format output
|
|
39
|
+
# Using a non-printable unit separator (US, 0x1F) to avoid collisions with
|
|
40
|
+
# stash messages and author/committer fields, while still working with
|
|
41
|
+
# Process.spawn (which doesn't allow NUL bytes in arguments)
|
|
42
|
+
FIELD_SEPARATOR = "\x1f"
|
|
43
|
+
|
|
44
|
+
# Custom format for git stash list that extracts all available metadata
|
|
45
|
+
# %H = full commit SHA
|
|
46
|
+
# %h = abbreviated commit SHA
|
|
47
|
+
# %gd = reflog selector (stash@\\{n})
|
|
48
|
+
# %gs = reflog subject (the stash message)
|
|
49
|
+
# %an = author name
|
|
50
|
+
# %ae = author email
|
|
51
|
+
# %aI = author date (ISO 8601 format, parsed into a Time)
|
|
52
|
+
# %cn = committer name
|
|
53
|
+
# %ce = committer email
|
|
54
|
+
# %cI = committer date (ISO 8601 format, parsed into a Time)
|
|
55
|
+
STASH_FORMAT = [
|
|
56
|
+
'%H', # 0: full SHA
|
|
57
|
+
'%h', # 1: short SHA
|
|
58
|
+
'%gd', # 2: reflog selector
|
|
59
|
+
'%gs', # 3: reflog subject (message)
|
|
60
|
+
'%an', # 4: author name
|
|
61
|
+
'%ae', # 5: author email
|
|
62
|
+
'%aI', # 6: author date
|
|
63
|
+
'%cn', # 7: committer name
|
|
64
|
+
'%ce', # 8: committer email
|
|
65
|
+
'%cI' # 9: committer date
|
|
66
|
+
].join(FIELD_SEPARATOR)
|
|
67
|
+
|
|
68
|
+
# Number of fields expected in the parsed output
|
|
69
|
+
FIELD_COUNT = 10
|
|
70
|
+
|
|
71
|
+
# Field indices for parsed output
|
|
72
|
+
module Fields
|
|
73
|
+
# Index of the full object ID (`%H`) field
|
|
74
|
+
OID = 0
|
|
75
|
+
|
|
76
|
+
# Index of the abbreviated object ID (`%h`) field
|
|
77
|
+
SHORT_OID = 1
|
|
78
|
+
|
|
79
|
+
# Index of the reflog selector (`%gd`) field
|
|
80
|
+
REFLOG = 2
|
|
81
|
+
|
|
82
|
+
# Index of the reflog subject (`%gs`) field
|
|
83
|
+
MESSAGE = 3
|
|
84
|
+
|
|
85
|
+
# Index of the author name (`%an`) field
|
|
86
|
+
AUTHOR_NAME = 4
|
|
87
|
+
|
|
88
|
+
# Index of the author email (`%ae`) field
|
|
89
|
+
AUTHOR_EMAIL = 5
|
|
90
|
+
|
|
91
|
+
# Index of the author date (`%aI`) field
|
|
92
|
+
AUTHOR_DATE = 6
|
|
93
|
+
|
|
94
|
+
# Index of the committer name (`%cn`) field
|
|
95
|
+
COMMITTER_NAME = 7
|
|
96
|
+
|
|
97
|
+
# Index of the committer email (`%ce`) field
|
|
98
|
+
COMMITTER_EMAIL = 8
|
|
99
|
+
|
|
100
|
+
# Index of the committer date (`%cI`) field
|
|
101
|
+
COMMITTER_DATE = 9
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Pattern to extract branch from standard stash messages
|
|
105
|
+
# Matches "WIP on <branch>:" or "On <branch>:" at the start
|
|
106
|
+
BRANCH_PATTERN = /^(?:WIP on|On)\s+([^:]+):/
|
|
107
|
+
|
|
108
|
+
module_function
|
|
109
|
+
|
|
110
|
+
# Parse git stash list output into StashInfo objects
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# StashParser.parse_list("abc123\x1fabc\x1fstash@\\{0}\x1fWIP on main: msg\x1f...\n")
|
|
114
|
+
# # => [#<Git::StashInfo index: 0, ...>]
|
|
115
|
+
#
|
|
116
|
+
# @param stdout [String] output from git stash list --format=...
|
|
117
|
+
#
|
|
118
|
+
# @return [Array<Git::StashInfo>] parsed stash information
|
|
119
|
+
#
|
|
120
|
+
# @raise [Git::UnexpectedResultError] if stash output cannot be parsed
|
|
121
|
+
#
|
|
122
|
+
def parse_list(stdout)
|
|
123
|
+
lines = stdout.split("\n")
|
|
124
|
+
lines.each_with_index.map { |line, idx| parse_stash_line(line, idx, lines) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Parse a single stash list line into a StashInfo object
|
|
128
|
+
#
|
|
129
|
+
# @param line [String] a line from git stash list output (custom format)
|
|
130
|
+
#
|
|
131
|
+
# @param expected_index [Integer] the expected stash index for validation
|
|
132
|
+
#
|
|
133
|
+
# @param all_lines [Array<String>] all output lines (for error messages)
|
|
134
|
+
#
|
|
135
|
+
# @return [Git::StashInfo] parsed stash info
|
|
136
|
+
#
|
|
137
|
+
# @raise [Git::UnexpectedResultError] if line format is unexpected
|
|
138
|
+
#
|
|
139
|
+
def parse_stash_line(line, expected_index, all_lines)
|
|
140
|
+
parts = line.split(FIELD_SEPARATOR, FIELD_COUNT)
|
|
141
|
+
return build_stash_info(parts, expected_index) if parts.length == FIELD_COUNT
|
|
142
|
+
|
|
143
|
+
raise Git::UnexpectedResultError, unexpected_stash_line_error(all_lines, line, expected_index)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Build a StashInfo from parsed format parts
|
|
147
|
+
#
|
|
148
|
+
# @param parts [Array<String>] the parsed format fields
|
|
149
|
+
#
|
|
150
|
+
# @param expected_index [Integer] fallback index if not parseable from reflog
|
|
151
|
+
#
|
|
152
|
+
# @return [Git::StashInfo]
|
|
153
|
+
#
|
|
154
|
+
def build_stash_info(parts, expected_index)
|
|
155
|
+
index = extract_index(parts[Fields::REFLOG]) || expected_index
|
|
156
|
+
|
|
157
|
+
Git::StashInfo.new(**stash_info_attrs(parts, index))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Build StashInfo attributes hash from parsed parts
|
|
161
|
+
#
|
|
162
|
+
# @param parts [Array<String>] the parsed format fields
|
|
163
|
+
#
|
|
164
|
+
# @param index [Integer] the resolved stash index
|
|
165
|
+
#
|
|
166
|
+
# @return [Hash] attributes for StashInfo.new
|
|
167
|
+
#
|
|
168
|
+
def stash_info_attrs(parts, index)
|
|
169
|
+
core_attrs(parts, index).merge(author: author_info(parts), committer: committer_info(parts))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Build core StashInfo attributes from parsed fields
|
|
173
|
+
#
|
|
174
|
+
# @param parts [Array<String>] the parsed format fields
|
|
175
|
+
#
|
|
176
|
+
# @param index [Integer] the resolved stash index
|
|
177
|
+
#
|
|
178
|
+
# @return [Hash<Symbol, Object>] core attributes for StashInfo.new
|
|
179
|
+
#
|
|
180
|
+
def core_attrs(parts, index)
|
|
181
|
+
{
|
|
182
|
+
index: index, name: parts[Fields::REFLOG], oid: parts[Fields::OID],
|
|
183
|
+
short_oid: parts[Fields::SHORT_OID], branch: extract_branch(parts[Fields::MESSAGE]),
|
|
184
|
+
message: parts[Fields::MESSAGE]
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Build the author identity from the parsed fields
|
|
189
|
+
#
|
|
190
|
+
# @param parts [Array<String>] the parsed format fields
|
|
191
|
+
#
|
|
192
|
+
# @return [Git::AuthorInfo] the stash author; its `date` is a `Time`
|
|
193
|
+
#
|
|
194
|
+
def author_info(parts)
|
|
195
|
+
build_author_info(parts[Fields::AUTHOR_NAME], parts[Fields::AUTHOR_EMAIL], parts[Fields::AUTHOR_DATE])
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Build the committer identity from the parsed fields
|
|
199
|
+
#
|
|
200
|
+
# @param parts [Array<String>] the parsed format fields
|
|
201
|
+
#
|
|
202
|
+
# @return [Git::AuthorInfo] the stash committer; its `date` is a `Time`
|
|
203
|
+
#
|
|
204
|
+
def committer_info(parts)
|
|
205
|
+
build_author_info(
|
|
206
|
+
parts[Fields::COMMITTER_NAME], parts[Fields::COMMITTER_EMAIL], parts[Fields::COMMITTER_DATE]
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Build a Git::AuthorInfo from identity fields
|
|
211
|
+
#
|
|
212
|
+
# The date is parsed with `Time.iso8601`, so the UTC offset git emits for
|
|
213
|
+
# `%aI` and `%cI` is preserved in the resulting `Time`.
|
|
214
|
+
#
|
|
215
|
+
# @param name [String] the `%an` or `%cn` field
|
|
216
|
+
#
|
|
217
|
+
# @param email [String] the `%ae` or `%ce` field
|
|
218
|
+
#
|
|
219
|
+
# @param date [String] the `%aI` or `%cI` field in ISO 8601 format
|
|
220
|
+
#
|
|
221
|
+
# @return [Git::AuthorInfo] the identity with `date` as a `Time`
|
|
222
|
+
#
|
|
223
|
+
# @raise [Git::UnexpectedResultError] if the date is not a valid ISO 8601 date
|
|
224
|
+
#
|
|
225
|
+
def build_author_info(name, email, date)
|
|
226
|
+
Git::AuthorInfo.new(name: name, email: email, date: parse_date(date))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Parse a `%aI` or `%cI` field into a Time
|
|
230
|
+
#
|
|
231
|
+
# @param date [String] the date field in ISO 8601 format
|
|
232
|
+
#
|
|
233
|
+
# @return [Time] the parsed time, preserving the UTC offset
|
|
234
|
+
#
|
|
235
|
+
# @raise [Git::UnexpectedResultError] if the field is not a valid ISO 8601 date
|
|
236
|
+
#
|
|
237
|
+
def parse_date(date)
|
|
238
|
+
Time.iso8601(date)
|
|
239
|
+
rescue ArgumentError => e
|
|
240
|
+
raise Git::UnexpectedResultError,
|
|
241
|
+
"Unexpected date #{date.inspect} in output from `git stash list`: #{e.message}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Extract the stash index from a reflog selector
|
|
245
|
+
#
|
|
246
|
+
# @param reflog_selector [String] e.g., "stash@\\{0}"
|
|
247
|
+
#
|
|
248
|
+
# @return [Integer, nil] the index or nil if not found
|
|
249
|
+
#
|
|
250
|
+
def extract_index(reflog_selector)
|
|
251
|
+
match = reflog_selector&.match(/stash@\{(\d+)\}/)
|
|
252
|
+
match ? match[1].to_i : nil
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Extract the branch name from a stash message
|
|
256
|
+
#
|
|
257
|
+
# @param message [String] the stash message
|
|
258
|
+
#
|
|
259
|
+
# @return [String, nil] the branch name or nil for custom messages
|
|
260
|
+
#
|
|
261
|
+
def extract_branch(message)
|
|
262
|
+
match = BRANCH_PATTERN.match(message)
|
|
263
|
+
match ? match[1] : nil
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Generate error message for unexpected stash line format
|
|
267
|
+
#
|
|
268
|
+
# @param lines [Array<String>] all output lines
|
|
269
|
+
#
|
|
270
|
+
# @param line [String] the problematic line
|
|
271
|
+
#
|
|
272
|
+
# @param index [Integer] the stash index
|
|
273
|
+
#
|
|
274
|
+
# @return [String] formatted error message
|
|
275
|
+
#
|
|
276
|
+
def unexpected_stash_line_error(lines, line, index)
|
|
277
|
+
format_str = STASH_FORMAT.gsub(FIELD_SEPARATOR, '<FS>')
|
|
278
|
+
<<~ERROR
|
|
279
|
+
Unexpected line in output from `git stash list --format=#{format_str}`, at index #{index}
|
|
280
|
+
|
|
281
|
+
Expected #{FIELD_COUNT} fields separated by '\\x1f' (unit separator), got #{line.split(FIELD_SEPARATOR, -1).length}
|
|
282
|
+
|
|
283
|
+
Full output:
|
|
284
|
+
#{lines.join("\n ")}
|
|
285
|
+
|
|
286
|
+
Line at index #{index}:
|
|
287
|
+
"#{line}"
|
|
288
|
+
ERROR
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'git/errors'
|
|
4
|
+
require 'git/status_file_info'
|
|
5
|
+
|
|
6
|
+
module Git
|
|
7
|
+
module Parsers
|
|
8
|
+
# Parser for `git status --porcelain=v2 -z` output
|
|
9
|
+
#
|
|
10
|
+
# Builds one {Git::StatusFileInfo} per entry. With `-z` every entry is
|
|
11
|
+
# NUL-terminated and paths are emitted verbatim (no quoting), so a path may
|
|
12
|
+
# contain spaces but never a NUL. The original path of a rename or copy
|
|
13
|
+
# entry is the NUL-terminated token that follows the entry.
|
|
14
|
+
#
|
|
15
|
+
# Every entry type of the porcelain v2 format is handled: `1` (ordinary),
|
|
16
|
+
# `2` (rename or copy), `u` (unmerged), `?` (untracked), and `!` (ignored).
|
|
17
|
+
# `#` header lines, emitted with `--branch` or `--show-stash`, are skipped.
|
|
18
|
+
#
|
|
19
|
+
# {Git::StatusFileInfo} lives at the top-level `Git::` namespace rather than
|
|
20
|
+
# within `Git::Parsers::` because it is public API returned to callers,
|
|
21
|
+
# while this parser is infrastructure.
|
|
22
|
+
#
|
|
23
|
+
# @example Parse the output of `git status --porcelain=v2 -z`
|
|
24
|
+
# Git::Parsers::Status.parse("? new.txt\0")
|
|
25
|
+
# #=> [#<data Git::StatusFileInfo path="new.txt", index_status="?", ...>]
|
|
26
|
+
#
|
|
27
|
+
# @see https://git-scm.com/docs/git-status#_porcelain_format_version_2
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
#
|
|
31
|
+
module Status
|
|
32
|
+
# Separator between entries (and between a rename entry and its original path)
|
|
33
|
+
ENTRY_SEPARATOR = "\0"
|
|
34
|
+
|
|
35
|
+
# Separator between the fields of one entry
|
|
36
|
+
FIELD_SEPARATOR = / /
|
|
37
|
+
|
|
38
|
+
# First character of an ordinary (changed, added, or deleted) entry
|
|
39
|
+
ORDINARY_ENTRY = '1'
|
|
40
|
+
|
|
41
|
+
# First character of a rename or copy entry
|
|
42
|
+
RENAMED_ENTRY = '2'
|
|
43
|
+
|
|
44
|
+
# First character of an unmerged entry
|
|
45
|
+
UNMERGED_ENTRY = 'u'
|
|
46
|
+
|
|
47
|
+
# First character of an untracked entry
|
|
48
|
+
UNTRACKED_ENTRY = '?'
|
|
49
|
+
|
|
50
|
+
# First character of an ignored entry
|
|
51
|
+
IGNORED_ENTRY = '!'
|
|
52
|
+
|
|
53
|
+
# First character of a header line
|
|
54
|
+
HEADER_LINE = '#'
|
|
55
|
+
|
|
56
|
+
# Field count of an ordinary entry: `1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>`
|
|
57
|
+
ORDINARY_FIELD_COUNT = 9
|
|
58
|
+
|
|
59
|
+
# Field count of a rename or copy entry, which adds `<X><score>` before the path
|
|
60
|
+
RENAMED_FIELD_COUNT = 10
|
|
61
|
+
|
|
62
|
+
# Field count of an unmerged entry: `u <XY> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>`
|
|
63
|
+
UNMERGED_FIELD_COUNT = 11
|
|
64
|
+
|
|
65
|
+
# Field count of an untracked or ignored entry: `? <path>` or `! <path>`
|
|
66
|
+
PATH_ONLY_FIELD_COUNT = 2
|
|
67
|
+
|
|
68
|
+
# Every {Git::StatusFileInfo} member set to `nil`, for entries that lack a field
|
|
69
|
+
#
|
|
70
|
+
# @return [Hash{Symbol => nil}]
|
|
71
|
+
#
|
|
72
|
+
EMPTY_MEMBERS = Git::StatusFileInfo.members.to_h { |member| [member, nil] }.freeze
|
|
73
|
+
|
|
74
|
+
module_function
|
|
75
|
+
|
|
76
|
+
# Parse `git status --porcelain=v2 -z` output into StatusFileInfo objects
|
|
77
|
+
#
|
|
78
|
+
# @example Parse two entries
|
|
79
|
+
# Git::Parsers::Status.parse(
|
|
80
|
+
# "1 .M N... 100644 100644 100644 #{sha} #{sha} lib/foo.rb\0? new.txt\0"
|
|
81
|
+
# ).map(&:path) #=> ["lib/foo.rb", "new.txt"]
|
|
82
|
+
#
|
|
83
|
+
# @param stdout [String] the NUL-separated output of `git status --porcelain=v2 -z`
|
|
84
|
+
#
|
|
85
|
+
# @return [Array<Git::StatusFileInfo>] one entry per reported path, in git's order
|
|
86
|
+
#
|
|
87
|
+
# @raise [Git::UnexpectedResultError] if an entry does not match the porcelain v2 format
|
|
88
|
+
#
|
|
89
|
+
def parse(stdout)
|
|
90
|
+
tokens = stdout.split(ENTRY_SEPARATOR)
|
|
91
|
+
files = []
|
|
92
|
+
until tokens.empty?
|
|
93
|
+
entry = tokens.shift
|
|
94
|
+
files << parse_entry(entry, tokens) unless entry.start_with?(HEADER_LINE)
|
|
95
|
+
end
|
|
96
|
+
files
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Parse one entry, consuming its original path from `tokens` for renames and copies
|
|
100
|
+
#
|
|
101
|
+
# @param entry [String] the entry line without its NUL terminator
|
|
102
|
+
#
|
|
103
|
+
# @param tokens [Array<String>] the entries that follow; a rename or copy
|
|
104
|
+
# entry's original path is shifted off the front
|
|
105
|
+
#
|
|
106
|
+
# @return [Git::StatusFileInfo] the parsed entry
|
|
107
|
+
#
|
|
108
|
+
# @raise [Git::UnexpectedResultError] if the entry type is not recognized
|
|
109
|
+
#
|
|
110
|
+
def parse_entry(entry, tokens)
|
|
111
|
+
case entry[0]
|
|
112
|
+
when ORDINARY_ENTRY then parse_ordinary(entry)
|
|
113
|
+
when RENAMED_ENTRY then parse_renamed(entry, tokens.shift)
|
|
114
|
+
when UNMERGED_ENTRY then parse_unmerged(entry)
|
|
115
|
+
when UNTRACKED_ENTRY, IGNORED_ENTRY then parse_path_only(entry)
|
|
116
|
+
else raise Git::UnexpectedResultError, unexpected_entry_error(entry)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Parse an ordinary (`1`) entry
|
|
121
|
+
#
|
|
122
|
+
# @param entry [String] the entry line
|
|
123
|
+
#
|
|
124
|
+
# @return [Git::StatusFileInfo] the parsed entry
|
|
125
|
+
#
|
|
126
|
+
# @raise [Git::UnexpectedResultError] if the entry does not have nine fields
|
|
127
|
+
#
|
|
128
|
+
def parse_ordinary(entry)
|
|
129
|
+
_type, xy, submodule, mode_head, mode_index, mode_worktree, sha_head, sha_index, path =
|
|
130
|
+
split_fields(entry, ORDINARY_FIELD_COUNT)
|
|
131
|
+
build_file_info(xy, path:, submodule:, mode_head:, mode_index:, mode_worktree:, sha_head:, sha_index:)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parse a rename or copy (`2`) entry
|
|
135
|
+
#
|
|
136
|
+
# @param entry [String] the entry line
|
|
137
|
+
#
|
|
138
|
+
# @param original_path [String, nil] the NUL-terminated token that followed
|
|
139
|
+
# the entry, or `nil` when the output ended
|
|
140
|
+
#
|
|
141
|
+
# @return [Git::StatusFileInfo] the parsed entry
|
|
142
|
+
#
|
|
143
|
+
# @raise [Git::UnexpectedResultError] if the entry does not have ten fields
|
|
144
|
+
# or the original path is missing
|
|
145
|
+
#
|
|
146
|
+
def parse_renamed(entry, original_path)
|
|
147
|
+
_type, xy, submodule, mode_head, mode_index, mode_worktree, sha_head, sha_index, score, path =
|
|
148
|
+
split_fields(entry, RENAMED_FIELD_COUNT)
|
|
149
|
+
raise Git::UnexpectedResultError, unexpected_entry_error(entry) if original_path.nil?
|
|
150
|
+
|
|
151
|
+
build_file_info(
|
|
152
|
+
xy,
|
|
153
|
+
path:, submodule:, mode_head:, mode_index:, mode_worktree:, sha_head:, sha_index:,
|
|
154
|
+
original_path:, rename_score: score[1..].to_i
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Parse an unmerged (`u`) entry
|
|
159
|
+
#
|
|
160
|
+
# @param entry [String] the entry line
|
|
161
|
+
#
|
|
162
|
+
# @return [Git::StatusFileInfo] the parsed entry with its stage data in
|
|
163
|
+
# `unmerged_stages`
|
|
164
|
+
#
|
|
165
|
+
# @raise [Git::UnexpectedResultError] if the entry does not have eleven fields
|
|
166
|
+
#
|
|
167
|
+
def parse_unmerged(entry)
|
|
168
|
+
_type, xy, submodule, mode1, mode2, mode3, mode_worktree, sha1, sha2, sha3, path =
|
|
169
|
+
split_fields(entry, UNMERGED_FIELD_COUNT)
|
|
170
|
+
stages = unmerged_stages([mode1, mode2, mode3], [sha1, sha2, sha3])
|
|
171
|
+
build_file_info(xy, path:, submodule:, mode_worktree:, unmerged_stages: stages)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Parse an untracked (`?`) or ignored (`!`) entry
|
|
175
|
+
#
|
|
176
|
+
# The entry's single status character is used for both status positions,
|
|
177
|
+
# matching the `??` and `!!` codes of the short format.
|
|
178
|
+
#
|
|
179
|
+
# @param entry [String] the entry line
|
|
180
|
+
#
|
|
181
|
+
# @return [Git::StatusFileInfo] the parsed entry with `nil` metadata
|
|
182
|
+
#
|
|
183
|
+
# @raise [Git::UnexpectedResultError] if the entry does not have two fields
|
|
184
|
+
#
|
|
185
|
+
def parse_path_only(entry)
|
|
186
|
+
type, path = split_fields(entry, PATH_ONLY_FIELD_COUNT)
|
|
187
|
+
build_file_info(type * 2, path: path)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Split an entry into exactly `count` fields, the last of which is the path
|
|
191
|
+
#
|
|
192
|
+
# The path may contain spaces, so the split is limited to `count` fields.
|
|
193
|
+
#
|
|
194
|
+
# @param entry [String] the entry line
|
|
195
|
+
#
|
|
196
|
+
# @param count [Integer] the number of fields the entry type has
|
|
197
|
+
#
|
|
198
|
+
# @return [Array<String>] the fields
|
|
199
|
+
#
|
|
200
|
+
# @raise [Git::UnexpectedResultError] if the entry has fewer fields
|
|
201
|
+
#
|
|
202
|
+
def split_fields(entry, count)
|
|
203
|
+
fields = entry.split(FIELD_SEPARATOR, count)
|
|
204
|
+
return fields if fields.length == count
|
|
205
|
+
|
|
206
|
+
raise Git::UnexpectedResultError, unexpected_entry_error(entry)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Build the frozen stage hash of an unmerged entry
|
|
210
|
+
#
|
|
211
|
+
# @param modes [Array<String>] the stage 1, 2, and 3 modes
|
|
212
|
+
#
|
|
213
|
+
# @param shas [Array<String>] the stage 1, 2, and 3 object names
|
|
214
|
+
#
|
|
215
|
+
# @return [Hash{Integer => Hash{Symbol => String}}] frozen `\\{ mode:, sha: }`
|
|
216
|
+
# hashes keyed by stage number
|
|
217
|
+
#
|
|
218
|
+
def unmerged_stages(modes, shas)
|
|
219
|
+
modes.zip(shas).each_with_index.to_h do |(mode, sha), index|
|
|
220
|
+
[index + 1, { mode: mode, sha: sha }.freeze]
|
|
221
|
+
end.freeze
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Build a {Git::StatusFileInfo}, defaulting every member not given to `nil`
|
|
225
|
+
#
|
|
226
|
+
# @param statuses [String] the two status characters, `X` then `Y`
|
|
227
|
+
#
|
|
228
|
+
# @param members [Hash{Symbol => Object}] the members that the entry provides
|
|
229
|
+
#
|
|
230
|
+
# @option members [String] :path the repository-relative path
|
|
231
|
+
#
|
|
232
|
+
# @return [Git::StatusFileInfo] the value object
|
|
233
|
+
#
|
|
234
|
+
def build_file_info(statuses, **members)
|
|
235
|
+
Git::StatusFileInfo.new(
|
|
236
|
+
**EMPTY_MEMBERS, index_status: statuses[0], worktree_status: statuses[1], **members
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Generate the error message for an entry that does not match the format
|
|
241
|
+
#
|
|
242
|
+
# @param entry [String] the offending entry line
|
|
243
|
+
#
|
|
244
|
+
# @return [String] the message
|
|
245
|
+
#
|
|
246
|
+
def unexpected_entry_error(entry)
|
|
247
|
+
"Unexpected entry in output from `git status --porcelain=v2 -z`: #{entry.inspect}"
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|