Module: Git::Parsers::Status Private

Defined in:
lib/git/parsers/status.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

Parser for git status --porcelain=v2 -z output

Builds one StatusFileInfo per entry. With -z every entry is NUL-terminated and paths are emitted verbatim (no quoting), so a path may contain spaces but never a NUL. The original path of a rename or copy entry is the NUL-terminated token that follows the entry.

Every entry type of the porcelain v2 format is handled: 1 (ordinary), 2 (rename or copy), u (unmerged), ? (untracked), and ! (ignored). # header lines, emitted with --branch or --show-stash, are skipped.

StatusFileInfo lives at the top-level Git:: namespace rather than within Git::Parsers:: because it is public API returned to callers, while this parser is infrastructure.

Examples:

Parse the output of git status --porcelain=v2 -z

Git::Parsers::Status.parse("? new.txt\0")
#=> [#<data Git::StatusFileInfo path="new.txt", index_status="?", ...>]

See Also:

Constant Summary collapse

ENTRY_SEPARATOR =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Separator between entries (and between a rename entry and its original path)

"\0"
FIELD_SEPARATOR =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Separator between the fields of one entry

/ /
ORDINARY_ENTRY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

First character of an ordinary (changed, added, or deleted) entry

'1'
RENAMED_ENTRY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

First character of a rename or copy entry

'2'
UNMERGED_ENTRY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

First character of an unmerged entry

'u'
UNTRACKED_ENTRY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

First character of an untracked entry

'?'
IGNORED_ENTRY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

First character of an ignored entry

'!'
HEADER_LINE =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

First character of a header line

'#'
ORDINARY_FIELD_COUNT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Field count of an ordinary entry: 1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>

9
RENAMED_FIELD_COUNT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Field count of a rename or copy entry, which adds <X><score> before the path

10
UNMERGED_FIELD_COUNT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Field count of an unmerged entry: u <XY> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>

11
PATH_ONLY_FIELD_COUNT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Field count of an untracked or ignored entry: ? <path> or ! <path>

2
EMPTY_MEMBERS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Every StatusFileInfo member set to nil, for entries that lack a field

Returns:

  • (Hash{Symbol => nil})
Git::StatusFileInfo.members.to_h { |member| [member, nil] }.freeze

Class Method Summary collapse

Class Method Details

.build_file_info(statuses, **members) ⇒ Git::StatusFileInfo

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build a StatusFileInfo, defaulting every member not given to nil

Parameters:

  • statuses (String)

    the two status characters, X then Y

  • members (Hash{Symbol => Object})

    the members that the entry provides

Options Hash (**members):

  • :path (String)

    the repository-relative path

Returns:



234
235
236
237
238
# File 'lib/git/parsers/status.rb', line 234

def build_file_info(statuses, **members)
  Git::StatusFileInfo.new(
    **EMPTY_MEMBERS, index_status: statuses[0], worktree_status: statuses[1], **members
  )
end

.parse(stdout) ⇒ Array<Git::StatusFileInfo>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse git status --porcelain=v2 -z output into StatusFileInfo objects

Examples:

Parse two entries

Git::Parsers::Status.parse(
  "1 .M N... 100644 100644 100644 #{sha} #{sha} lib/foo.rb\0? new.txt\0"
).map(&:path) #=> ["lib/foo.rb", "new.txt"]

Parameters:

  • stdout (String)

    the NUL-separated output of git status --porcelain=v2 -z

Returns:

Raises:



89
90
91
92
93
94
95
96
97
# File 'lib/git/parsers/status.rb', line 89

def parse(stdout)
  tokens = stdout.split(ENTRY_SEPARATOR)
  files = []
  until tokens.empty?
    entry = tokens.shift
    files << parse_entry(entry, tokens) unless entry.start_with?(HEADER_LINE)
  end
  files
end

.parse_entry(entry, tokens) ⇒ Git::StatusFileInfo

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse one entry, consuming its original path from tokens for renames and copies

Parameters:

  • entry (String)

    the entry line without its NUL terminator

  • tokens (Array<String>)

    the entries that follow; a rename or copy entry's original path is shifted off the front

Returns:

Raises:



110
111
112
113
114
115
116
117
118
# File 'lib/git/parsers/status.rb', line 110

def parse_entry(entry, tokens)
  case entry[0]
  when ORDINARY_ENTRY then parse_ordinary(entry)
  when RENAMED_ENTRY then parse_renamed(entry, tokens.shift)
  when UNMERGED_ENTRY then parse_unmerged(entry)
  when UNTRACKED_ENTRY, IGNORED_ENTRY then parse_path_only(entry)
  else raise Git::UnexpectedResultError, unexpected_entry_error(entry)
  end
end

.parse_ordinary(entry) ⇒ Git::StatusFileInfo

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse an ordinary (1) entry

Parameters:

  • entry (String)

    the entry line

Returns:

Raises:



128
129
130
131
132
# File 'lib/git/parsers/status.rb', line 128

def parse_ordinary(entry)
  _type, xy, submodule, mode_head, mode_index, mode_worktree, sha_head, sha_index, path =
    split_fields(entry, ORDINARY_FIELD_COUNT)
  build_file_info(xy, path:, submodule:, mode_head:, mode_index:, mode_worktree:, sha_head:, sha_index:)
end

.parse_path_only(entry) ⇒ Git::StatusFileInfo

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse an untracked (?) or ignored (!) entry

The entry's single status character is used for both status positions, matching the ?? and !! codes of the short format.

Parameters:

  • entry (String)

    the entry line

Returns:

Raises:



185
186
187
188
# File 'lib/git/parsers/status.rb', line 185

def parse_path_only(entry)
  type, path = split_fields(entry, PATH_ONLY_FIELD_COUNT)
  build_file_info(type * 2, path: path)
end

.parse_renamed(entry, original_path) ⇒ Git::StatusFileInfo

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse a rename or copy (2) entry

Parameters:

  • entry (String)

    the entry line

  • original_path (String, nil)

    the NUL-terminated token that followed the entry, or nil when the output ended

Returns:

Raises:



146
147
148
149
150
151
152
153
154
155
156
# File 'lib/git/parsers/status.rb', line 146

def parse_renamed(entry, original_path)
  _type, xy, submodule, mode_head, mode_index, mode_worktree, sha_head, sha_index, score, path =
    split_fields(entry, RENAMED_FIELD_COUNT)
  raise Git::UnexpectedResultError, unexpected_entry_error(entry) if original_path.nil?

  build_file_info(
    xy,
    path:, submodule:, mode_head:, mode_index:, mode_worktree:, sha_head:, sha_index:,
    original_path:, rename_score: score[1..].to_i
  )
end

.parse_unmerged(entry) ⇒ Git::StatusFileInfo

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse an unmerged (u) entry

Parameters:

  • entry (String)

    the entry line

Returns:

Raises:



167
168
169
170
171
172
# File 'lib/git/parsers/status.rb', line 167

def parse_unmerged(entry)
  _type, xy, submodule, mode1, mode2, mode3, mode_worktree, sha1, sha2, sha3, path =
    split_fields(entry, UNMERGED_FIELD_COUNT)
  stages = unmerged_stages([mode1, mode2, mode3], [sha1, sha2, sha3])
  build_file_info(xy, path:, submodule:, mode_worktree:, unmerged_stages: stages)
end

.split_fields(entry, count) ⇒ Array<String>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Split an entry into exactly count fields, the last of which is the path

The path may contain spaces, so the split is limited to count fields.

Parameters:

  • entry (String)

    the entry line

  • count (Integer)

    the number of fields the entry type has

Returns:

  • (Array<String>)

    the fields

Raises:



202
203
204
205
206
207
# File 'lib/git/parsers/status.rb', line 202

def split_fields(entry, count)
  fields = entry.split(FIELD_SEPARATOR, count)
  return fields if fields.length == count

  raise Git::UnexpectedResultError, unexpected_entry_error(entry)
end

.unexpected_entry_error(entry) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Generate the error message for an entry that does not match the format

Parameters:

  • entry (String)

    the offending entry line

Returns:

  • (String)

    the message



246
247
248
# File 'lib/git/parsers/status.rb', line 246

def unexpected_entry_error(entry)
  "Unexpected entry in output from `git status --porcelain=v2 -z`: #{entry.inspect}"
end

.unmerged_stages(modes, shas) ⇒ Hash{Integer => Hash{Symbol => String}}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build the frozen stage hash of an unmerged entry

Parameters:

  • modes (Array<String>)

    the stage 1, 2, and 3 modes

  • shas (Array<String>)

    the stage 1, 2, and 3 object names

Returns:

  • (Hash{Integer => Hash{Symbol => String}})

    frozen \\{ mode:, sha: } hashes keyed by stage number



218
219
220
221
222
# File 'lib/git/parsers/status.rb', line 218

def unmerged_stages(modes, shas)
  modes.zip(shas).each_with_index.to_h do |(mode, sha), index|
    [index + 1, { mode: mode, sha: sha }.freeze]
  end.freeze
end