Module: Git::Parsers::Tag Private

Defined in:
lib/git/parsers/tag.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.

Note:

Known limitation: If a tag message contains the field delimiter character (\x1f, ASCII unit separator), it will be preserved correctly since the message is the last field. However, messages are rarely crafted with non-printable control characters.

Parser for git tag command output

Handles parsing of git tag --list and git tag --delete output into structured data objects.

Design Note: Namespace Organization

This parser creates and returns TagInfo and TagDeleteResult objects, which live at the top-level Git:: namespace rather than within Git::Parsers::. This is intentional:

  • Parsers are infrastructure - marked @api private, users shouldn't interact with them directly
  • Info/Result classes are public API - returned by commands and used throughout the codebase
  • Info classes are domain entities - represent core git concepts (tags as data)
  • Result classes are operation outcomes - represent command results, not parsing details

Keeping Info/Result classes at Git:: improves discoverability and correctly reflects their role as public types rather than parser internals.

Constant Summary collapse

FIELD_DELIMITER =

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.

Delimiter for separating fields in git tag --format output Field separator used in custom format output Using the ASCII unit separator (US, 0x1F / "\x1f"), a non-printable character, minimizes the chance of collisions with tag names or messages and remains safe to pass through Process.spawn and shell argument boundaries.

"\x1f"
RECORD_DELIMITER =

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.

Delimiter for separating records (tags) in output Using the ASCII record separator (RS, 0x1E / "\x1e") to delimit complete tag records. This allows multi-line messages (which contain newlines) to be parsed correctly since we split by record separator first, then by field delimiter.

"\x1e"
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.

Number of fields expected in the parsed output

8
FORMAT_STRING =

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.

Format string for git tag --format

Fields:

  • %(refname:short) - tag name
  • %(objectname) - SHA of the tag object (for annotated) or commit (for lightweight)
  • %(*objectname) - Dereferenced SHA (commit ID for annotated tags, empty for lightweight)
  • %(objecttype) - 'tag' for annotated tags, target object type (commit/tree/blob/etc.) for lightweight tags
  • %(taggername) - tagger name (empty for lightweight tags)
  • %(taggeremail) - tagger email (empty for lightweight tags)
  • %(taggerdate:iso8601-strict) - tagger date in strict ISO 8601 format
  • %(contents) - full tag message (can be multi-line)

Each tag record is terminated by the RECORD_DELIMITER to allow multi-line messages.

[
  '%(refname:short)',
  '%(objectname)',
  '%(*objectname)',
  '%(objecttype)',
  '%(taggername)',
  '%(taggeremail)',
  '%(taggerdate:iso8601-strict)',
  '%(contents)'
].join(FIELD_DELIMITER) + RECORD_DELIMITER
DELETED_TAG_REGEX =

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.

Regex to parse successful deletion lines from stdout Matches: Deleted tag 'tagname' (was abc123)

/^Deleted tag '([^']+)'/
ERROR_TAG_REGEX =

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.

Regex to parse error messages from stderr Matches: error: tag 'tagname' not found.

/^error: tag '([^']+)'(.*)$/

Class Method Summary collapse

Class Method Details

.build_delete_result(requested_names, existing_tags, deleted_names, error_map) ⇒ Git::TagDeleteResult

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 TagDeleteResult from parsed data

Parameters:

  • requested_names (Array<String>)

    originally requested tag names

  • existing_tags (Hash<String, Git::TagInfo>)

    tags that existed before delete

  • deleted_names (Array<String>)

    names confirmed deleted in stdout

  • error_map (Hash<String, String>)

    map of tag name to error message

Returns:



304
305
306
307
308
309
310
311
312
313
# File 'lib/git/parsers/tag.rb', line 304

def build_delete_result(requested_names, existing_tags, deleted_names, error_map)
  deleted = deleted_names.filter_map { |name| existing_tags[name] }

  not_deleted = (requested_names - deleted_names).map do |name|
    error_message = error_map[name] || "tag '#{name}' could not be deleted"
    Git::TagDeleteFailure.new(name: name, error_message: error_message)
  end

  Git::TagDeleteResult.new(deleted: deleted, not_deleted: not_deleted)
end

.build_tag_info(parts) ⇒ Git::TagInfo

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.

Note:

For annotated tags:

  • oid = %(objectname) (the tag object's ID)
  • target_oid = %(*objectname) (the dereferenced commit ID)
Note:

For lightweight tags:

  • oid = nil (lightweight tags are not objects)
  • target_oid = %(objectname) (the commit ID)

Build a TagInfo object from parsed parts

Parameters:

  • parts (Array<String>)

    the parsed format fields

Returns:



156
157
158
159
# File 'lib/git/parsers/tag.rb', line 156

def build_tag_info(parts)
  oid, target_oid = resolve_oids(parts[3], parts[1], parts[2])
  build_tag_info_object(parts, oid, target_oid)
end

.build_tag_info_object(parts, oid, target_oid) ⇒ Git::TagInfo

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.

Builds a TagInfo object from normalized parser values

Parameters:

  • parts (Array<String>)

    the parsed format fields

  • oid (String, nil)

    the tag object's OID or nil for lightweight tags

  • target_oid (String)

    the target object OID

Returns:



186
187
188
189
190
191
# File 'lib/git/parsers/tag.rb', line 186

def build_tag_info_object(parts, oid, target_oid)
  Git::TagInfo.new(
    name: parts[0], oid: oid, target_oid: target_oid, objecttype: parts[3],
    tagger: parse_tagger(parts[4], parts[5], parts[6]), message: parse_message(parts[3], parts[7])
  )
end

.parse_date(date) ⇒ Time

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 %(taggerdate:iso8601-strict) field into a Time

Parameters:

  • date (String)

    the date field in strict ISO 8601 format

Returns:

  • (Time)

    the parsed time, preserving the UTC offset

Raises:



240
241
242
243
244
245
# File 'lib/git/parsers/tag.rb', line 240

def parse_date(date)
  Time.iso8601(date)
rescue ArgumentError => e
  raise Git::UnexpectedResultError,
        "Unexpected tagger date #{date.inspect} in output from `git tag --list`: #{e.message}"
end

.parse_deleted_tags(stdout) ⇒ 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.

Parse deleted tag names from stdout

Examples:

TagParser.parse_deleted_tags("Deleted tag 'v1.0.0' (was abc123)\n")
# => ["v1.0.0"]

Parameters:

  • stdout (String)

    command stdout

Returns:

  • (Array<String>)

    names of successfully deleted tags



271
272
273
# File 'lib/git/parsers/tag.rb', line 271

def parse_deleted_tags(stdout)
  stdout.scan(DELETED_TAG_REGEX).flatten
end

.parse_error_messages(stderr) ⇒ Hash<String, 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.

Parse error messages from stderr into a map

Examples:

TagParser.parse_error_messages("error: tag 'missing' not found.\n")
# => {"missing" => "error: tag 'missing' not found."}

Parameters:

  • stderr (String)

    command stderr

Returns:

  • (Hash<String, String>)

    map of tag name to error message



285
286
287
288
289
290
# File 'lib/git/parsers/tag.rb', line 285

def parse_error_messages(stderr)
  stderr.each_line.with_object({}) do |line, hash|
    match = line.match(ERROR_TAG_REGEX)
    hash[match[1]] = line.strip if match
  end
end

.parse_list(stdout) ⇒ Array<Git::TagInfo>

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 tag --list output into TagInfo objects

Examples:

TagParser.parse_list("v1.0.0\x1f...\x1e\n")
# => [#<Git::TagInfo name: "v1.0.0", ...>]

Parameters:

  • stdout (String)

    output from git tag --list --format=...

Returns:

Raises:



105
106
107
108
109
110
111
# File 'lib/git/parsers/tag.rb', line 105

def parse_list(stdout)
  # Split by record separator
  # Each record may have a leading newline from the previous record's %(contents) output
  # Use lstrip to remove leading whitespace (which includes the newline) from each record
  records = stdout.split(RECORD_DELIMITER).map(&:lstrip).reject(&:empty?)
  records.map.with_index { |record, index| parse_tag_record(record, index, records) }
end

.parse_message(objecttype, message) ⇒ 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.

Parse message field, returning nil for lightweight tags or empty messages Strips trailing newlines that git adds to %(contents) output

Parameters:

  • objecttype (String)

    the object type ('tag' or 'commit')

  • message (String)

    the raw message field

Returns:

  • (String, nil)

    the message or nil



256
257
258
259
# File 'lib/git/parsers/tag.rb', line 256

def parse_message(objecttype, message)
  stripped = message.chomp
  objecttype == 'tag' && !stripped.empty? ? stripped : nil
end

.parse_tag_record(record, index, all_records) ⇒ Git::TagInfo

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 single formatted tag record

The record format is: nameshaderefobjecttypetagger_nametagger_emailtagger_datemessage where is the unit separator character ("\x1f").

For lightweight tags, Git emits empty strings for the tagger fields and message; these are converted to nil by #parse_tagger and #parse_message.

Parameters:

  • record (String)

    a single tag record from git tag --format output

  • index (Integer)

    record index for error reporting

  • all_records (Array<String>)

    all output records for error messages

Returns:

Raises:



132
133
134
135
136
137
138
139
140
# File 'lib/git/parsers/tag.rb', line 132

def parse_tag_record(record, index, all_records)
  parts = record.split(FIELD_DELIMITER, FIELD_COUNT)

  unless parts.length == FIELD_COUNT
    raise Git::UnexpectedResultError, unexpected_tag_record_error(all_records, record, index)
  end

  build_tag_info(parts)
end

.parse_tagger(name, email, date) ⇒ Git::AuthorInfo?

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 tagger identity from the tagger name, email, and date fields

Git emits empty strings for all three fields when there is no tag object (lightweight tags) or the tag object has no tagger header, in which case the tagger is nil. Otherwise the angle brackets git wraps around %(taggeremail) are stripped and the strict ISO 8601 %(taggerdate:iso8601-strict) value is parsed into a Time that preserves the UTC offset. A partially populated identity (for example an empty name with an email and date) is kept as emitted rather than dropped, and an empty date becomes nil.

Examples:

An annotated tag's tagger

parse_tagger('John Doe', '<[email protected]>', '2024-01-15T10:30:00-08:00')
#=> #<data Git::AuthorInfo name="John Doe", email="[email protected]", ...>

A lightweight tag has no tagger

parse_tagger('', '', '') #=> nil

Parameters:

  • name (String)

    the %(taggername) field

  • email (String)

    the %(taggeremail) field, including angle brackets

  • date (String)

    the %(taggerdate:iso8601-strict) field

Returns:

  • (Git::AuthorInfo, nil)

    the tagger, or nil when all three fields are empty

Raises:



222
223
224
225
226
227
228
229
230
# File 'lib/git/parsers/tag.rb', line 222

def parse_tagger(name, email, date)
  return nil if [name, email, date].all?(&:empty?)

  Git::AuthorInfo.new(
    name: name,
    email: email.delete_prefix('<').delete_suffix('>'),
    date: date.empty? ? nil : parse_date(date)
  )
end

.resolve_oids(objecttype, objectname, dereferenced) ⇒ Array((String, nil), 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.

Resolves canonical and target object OIDs from git tag format fields

Parameters:

  • objecttype (String)

    the object type from git output

  • objectname (String)

    the object OID from %(objectname)

  • dereferenced (String)

    the object OID from %(*objectname)

Returns:

  • (Array((String, nil), String))

    the two-element tuple [oid, target_oid]



172
173
174
# File 'lib/git/parsers/tag.rb', line 172

def resolve_oids(objecttype, objectname, dereferenced)
  objecttype == 'tag' ? [objectname, dereferenced] : [nil, objectname]
end

.unexpected_tag_record_error(records, record, index) ⇒ 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 error message for unexpected tag record format

Parameters:

  • records (Array<String>)

    all output records

  • record (String)

    the problematic record

  • index (Integer)

    the record index

Returns:

  • (String)

    formatted error message



325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/git/parsers/tag.rb', line 325

def unexpected_tag_record_error(records, record, index)
  format_str = FORMAT_STRING.gsub(FIELD_DELIMITER, '<FS>').gsub(RECORD_DELIMITER, '<RS>')
  <<~ERROR
    Unexpected record in output from `git tag --list --format=#{format_str}`, at index #{index}

    Expected #{FIELD_COUNT} fields separated by '\\x1f' (unit separator), got #{record.split(FIELD_DELIMITER, -1).length}

    Full output:
      #{records.join("\n  ")}

    Record at index #{index}:
      "#{record}"
  ERROR
end