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