lib/git_ls.rb in git_ls-0.2.0 vs lib/git_ls.rb in git_ls-0.3.0

- old
+ new

@@ -1,20 +1,139 @@ # frozen_string_literal: true -require_relative 'git_ls/parser' - -# Entry point for gem. # Usage: # GitLS.files -> Array of strings as files. # This will be identical output to git ls-files module GitLS class Error < StandardError; end class << self def files(path = ::Dir.pwd) + read(path, false) + end + + def headers(path = ::Dir.pwd) + read(path, true) + end + + private + + def read(path, return_headers_only) path = ::File.join(path, '.git/index') if ::File.directory?(path) - ::GitLS::Parser.new(::File.new(path)).files + file = ::File.new(path) + # 4-byte signature: + # The signature is { 'D', 'I', 'R', 'C' } (stands for "dircache") + # 4-byte version number: + # The current supported versions are 2, 3 and 4. + # 32-bit number of index entries. + sig, git_index_version, length = file.read(12).unpack('A4NN') + raise ::GitLS::Error, 'not a git dir or .git/index file' unless sig == 'DIRC' + + return { git_index_version: git_index_version, length: length } if return_headers_only + + files = Array.new(length) + case git_index_version + when 2 then files_2(files, file) + when 3 then files_3(files, file) + when 4 then files_4(files, file) + else raise ::GitLS::Error, 'Unrecognized git index version' + end + files rescue Errno::ENOENT => e raise GitLS::Error, "Not a git directory: #{e.message}" + ensure + # :nocov: + # coverage tracking for branches in ensure blocks is weird + file&.close + # :nocov: + files + end + + private + + def files_2(files, file) + files.map! do + file.pos += 60 # skip 60 bytes (40 bytes of stat, 20 bytes of sha) + length = (file.getbyte & 0b0000_1111) * 256 + file.getbyte # find the 12 byte length + if length < 0xFFF + path = file.read(length) + # :nocov: + else + # i can't test this i just get ENAMETOOLONG a lot + path = file.readline("\0").chop + file.pos -= 1 + # :nocov: + end + file.pos += 8 - ((length - 2) % 8) # 1-8 bytes padding of nuls + path + end + end + + def files_3(files, file) + files.map! do + file.pos += 60 # skip 60 bytes (40 bytes of stat, 20 bytes of sha) + + flags = file.getbyte * 256 + file.getbyte + extended_flag = (flags & 0b0100_0000_0000_0000).positive? + file.pos += 2 if extended_flag + + length = flags & 0b0000_1111_1111_1111 + if length < 0xFFF + path = file.read(length) + # :nocov: + else + # i can't test this i just get ENAMETOOLONG a lot + path = file.readline("\0").chop + file.pos -= 1 + # :nocov: + end + + file.pos += 8 - ((path.bytesize - (extended_flag ? 0 : 2)) % 8) # 1-8 bytes padding of nuls + path + end + end + + def files_4(files, file) + prev_entry_path = "" + files.map! do + file.pos += 60 # skip 60 bytes (40 bytes of stat, 20 bytes of sha) + flags = file.getbyte * 256 + file.getbyte + file.pos += 2 if (flags & 0b0100_0000_0000_0000).positive? + + length = flags & 0b0000_1111_1111_1111 + + # documentation for this number from + # https://git-scm.com/docs/pack-format#_original_version_1_pack_idx_files_have_the_following_format + # offset encoding: + # n bytes with MSB set in all but the last one. + # The offset is then the number constructed by + # concatenating the lower 7 bit of each byte, and + # for n >= 2 adding 2^7 + 2^14 + ... + 2^(7*(n-1)) + # to the result. + read_offset = 0 + prev_read_offset = file.getbyte + n = 1 + while (prev_read_offset & 0b1000_0000).positive? + read_offset += (prev_read_offset - 0b1000_0000) + read_offset += 2**(7 * n) + n += 1 + prev_read_offset = file.getbyte + end + read_offset += prev_read_offset + + initial_part_length = prev_entry_path.bytesize - read_offset + + if length < 0xFFF + rest = file.read(length - initial_part_length) + file.pos += 1 # the NUL + # :nocov: + else + # i can't test this i just get ENAMETOOLONG a lot + rest = file.readline("\0").chop + # :nocov: + end + + prev_entry_path = prev_entry_path.byteslice(0, initial_part_length) + rest + end end end end