module Git::Parsers::Tag
Parser for git tag command output
Handles parsing of âgit tag âlist` and `git tag âdelete` output into structured data objects.
@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.
## Design Note: Namespace Organization
This parser creates and returns {Git::TagInfo} and {Git::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.
@api private
Constants
- DELETED_TAG_REGEX
-
Regex to parse successful deletion lines from stdout Matches: Deleted tag âtagnameâ (was abc123)
- ERROR_TAG_REGEX
-
Regex to parse error messages from stderr Matches: error: tag âtagnameâ not found.
- FIELD_COUNT
-
Number of fields expected in the parsed output
- FIELD_DELIMITER
-
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.
- FORMAT_STRING
-
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_DELIMITERto allow multi-line messages. -
- RECORD_DELIMITER
-
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.
Public Instance Methods
Source
# File lib/git/parsers/tag.rb, line 258 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 the TagDeleteResult from parsed data
@param requested_names [Array<String>] originally requested tag names
@param existing_tags [Hash<String, Git::TagInfo>] tags that existed before delete
@param deleted_names [Array<String>] names confirmed deleted in stdout
@param error_map [Hash<String, String>] map of tag name to error message
@return [Git::TagDeleteResult] the result object
Source
# File lib/git/parsers/tag.rb, line 153 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 a TagInfo object from parsed parts
@param parts [Array<String>] the parsed format fields
@return [Git::TagInfo]
@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)
Source
# File lib/git/parsers/tag.rb, line 183 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_name: parse_optional_field(parts[4]), tagger_email: parse_optional_field(parts[5]), tagger_date: parse_optional_field(parts[6]), message: parse_message(parts[3], parts[7]) ) end
Builds a TagInfo object from normalized parser values
@param parts [Array<String>] the parsed format fields
@param oid [String, nil] the tag objectâs OID or nil for lightweight tags
@param target_oid [String] the target object OID
@return [Git::TagInfo] the tag info with all fields populated
Source
# File lib/git/parsers/tag.rb, line 239 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 error messages from stderr into a map
@example
TagParser.parse_error_messages("error: tag 'missing' not found.\n") # => {"missing" => "error: tag 'missing' not found."}
@param stderr [String] command stderr
@return [Hash<String, String>] map of tag name to error message
Source
# File lib/git/parsers/tag.rb, line 102 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 git tag âlist output into TagInfo objects
@example
TagParser.parse_list("v1.0.0\x1f...\x1e\n") # => [#<Git::TagInfo name: "v1.0.0", ...>]
@param stdout [String] output from git tag âlist âformat=âŚ
@return [Array<Git::TagInfo>] parsed tag information
@raise [Git::UnexpectedResultError] if any record has unexpected format
Source
# File lib/git/parsers/tag.rb, line 210 def parse_message(objecttype, message) stripped = message.chomp objecttype == 'tag' && !stripped.empty? ? stripped : nil end
Parse message field, returning nil for lightweight tags or empty messages Strips trailing newlines that git adds to %(contents) output
@param objecttype [String] the object type (âtagâ or âcommitâ)
@param message [String] the raw message field
@return [String, nil] the message or nil
Source
# File lib/git/parsers/tag.rb, line 197 def parse_optional_field(value) value.empty? ? nil : value end
Parse an optional field, returning nil if empty
@param value [String] the field value
@return [String, nil] the value or nil if empty
Source
# File lib/git/parsers/tag.rb, line 129 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 a single formatted tag record
The record format is:
name<FS>sha<FS>deref<FS>objecttype<FS>tagger_name<FS>tagger_email<FS>tagger_date<FS>message
where <FS> 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_optional_field} and {#parse_message}.
@param record [String] a single tag record from git tag âformat output
@param index [Integer] record index for error reporting
@param all_records [Array<String>] all output records for error messages
@return [Git::TagInfo] tag info with all fields populated
@raise [Git::UnexpectedResultError] if record format is unexpected
Source
# File lib/git/parsers/tag.rb, line 169 def resolve_oids(objecttype, objectname, dereferenced) objecttype == 'tag' ? [objectname, dereferenced] : [nil, objectname] end
Resolves canonical and target object OIDs from git tag format fields
@param objecttype [String] the object type from git output
@param objectname [String] the object OID from %(objectname)
@param dereferenced [String] the object OID from %(*objectname)
@return [Array((String, nil), String)] the two-element tuple
`[oid, target_oid]`
Source
# File lib/git/parsers/tag.rb, line 279 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
Generate error message for unexpected tag record format
@param records [Array<String>] all output records
@param record [String] the problematic record
@param index [Integer] the record index
@return [String] formatted error message