lib/git_ls.rb in git_ls-0.3.0 vs lib/git_ls.rb in git_ls-0.4.0
- old
+ new
@@ -1,11 +1,11 @@
# frozen_string_literal: true
# Usage:
# GitLS.files -> Array of strings as files.
# This will be identical output to git ls-files
-module GitLS
+module GitLS # rubocop:disable Metrics/ModuleLength
class Error < StandardError; end
class << self
def files(path = ::Dir.pwd)
read(path, false)
@@ -13,49 +13,136 @@
def headers(path = ::Dir.pwd)
read(path, true)
end
- private
+ private
- def read(path, return_headers_only)
+ def read(path, return_headers_only) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
path = ::File.join(path, '.git/index') if ::File.directory?(path)
file = ::File.new(path)
+ buf = ::String.new
# 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'
+ sig, git_index_version, length = file.read(12, buf).unpack('a4NN')
+ raise ::GitLS::Error, ".git/index file not found at #{path}" unless sig == 'DIRC'
return { git_index_version: git_index_version, length: length } if return_headers_only
- files = Array.new(length)
+ 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
+
+ extensions(files, file, buf)
files
- rescue Errno::ENOENT => e
- raise GitLS::Error, "Not a git directory: #{e.message}"
+ 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 extensions(files, file, buf)
+ case file.read(4, buf)
+ when 'link' then link_extension(files, file, buf)
+ when /[A-Z]{4}/ then ignored_extension(files, file, buf)
+ else
+ return if (file.pos += 16) && file.eof?
- def files_2(files, file)
+ raise ::GitLS::Error, "Unrecognized .git/index extension #{buf.inspect}"
+ end
+ end
+
+ def ignored_extension(files, file, buf)
+ size = file.read(4, buf).unpack1('N')
+ file.pos += size
+ extensions(files, file, buf)
+ end
+
+ def link_extension(files, file, buf) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
+ file.pos += 4 # size = file.read(4, buf).unpack1('N')
+
+ sha = file.read(20, buf)
+
+ new_files = files.dup
+
+ files.replace files("#{::File.dirname(file.path)}/sharedindex.#{sha.unpack1('H*')}")
+
+ ewah_each_value(file, buf) do |pos|
+ files[pos] = nil
+ end
+
+ ewah_each_value(file, buf) do |pos|
+ replacement_file = new_files.shift
+ # the documentation *implies* that this *may* get a new filename
+ # i can't get it to happen though
+ # :nocov:
+ files[pos] = replacement_file unless replacement_file.empty?
+ # :nocov:
+ end
+
+ files.compact!
+ files.concat(new_files)
+ files.sort!
+
+ extensions(files, file, buf)
+ end
+
+ # format is defined here:
+ # https://git-scm.com/docs/bitmap-format#_appendix_a_serialization_format_for_an_ewah_bitmap
+ def ewah_each_value(file, buf) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
+ uncompressed_pos = 0
+ file.pos += 4 # uncompressed_bits_count = file.read(4, buf).unpack1('N')
+ compressed_bytes = file.read(4, buf).unpack1('N') * 8
+
+ final_file_pos = file.pos + compressed_bytes
+
+ until file.pos == final_file_pos
+ run_length_word = file.read(8, buf).unpack1('Q>')
+ # 1st bit
+ run_bit = run_length_word & 1
+ # the next 32 bits, masked, multiplied by 64 (which is shifted by 6 places)
+ run_length = ((run_length_word >> 1) & 0xFFFF_FFFF) << 6
+ # the next 31 bits
+ literal_length = (run_length_word >> 33)
+
+ if run_bit == 1
+ run_length.times do
+ yield uncompressed_pos
+ uncompressed_pos += 1
+ end
+ else
+ uncompressed_pos += run_length
+ end
+
+ literal_length.times do
+ word = file.read(8, buf).unpack1('B*').reverse
+ word.each_char do |char|
+ yield(uncompressed_pos) if char == '1'
+
+ uncompressed_pos += 1
+ end
+ end
+ end
+
+ file.pos += 4 # bitmap metadata for adding to bitmaps
+ end
+
+ def files_2(files, file) # rubocop:disable Metrics/MethodLength
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
+ length = ((file.getbyte & 0b0000_1111) << 8) + 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
@@ -66,11 +153,11 @@
file.pos += 8 - ((length - 2) % 8) # 1-8 bytes padding of nuls
path
end
end
- def files_3(files, file)
+ def files_3(files, file) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
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?
@@ -90,11 +177,11 @@
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 = ""
+ def files_4(files, file) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
+ 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?