Compatible Android
This commit is contained in:
@ -0,0 +1,121 @@
|
||||
local util = require 'git.util'
|
||||
|
||||
local assert, next, io, print, os, type, string, pairs, tostring =
|
||||
assert, next, io, print, os, type, string, pairs, tostring
|
||||
local join_path = git.util.join_path
|
||||
|
||||
local require = require
|
||||
|
||||
local isPosix = package.config:sub(1,1) == '/' -- wild guess
|
||||
|
||||
module(...)
|
||||
|
||||
Commit = {}
|
||||
Commit.__index = Commit
|
||||
|
||||
function Commit:tree()
|
||||
return self.repo:tree(self.tree_sha)
|
||||
end
|
||||
|
||||
function Commit:checkout(path)
|
||||
assert(path, 'path argument missing')
|
||||
self:tree():checkoutTo(path)
|
||||
end
|
||||
|
||||
|
||||
Tree = {}
|
||||
Tree.__index = function (t,k)
|
||||
if Tree[k] then return Tree[k] end
|
||||
return t:entry(k)
|
||||
end
|
||||
|
||||
function Tree:entries()
|
||||
return function(t, n)
|
||||
local n, entry = next(t, n)
|
||||
if entry then
|
||||
local object
|
||||
if entry.type == 'tree' then
|
||||
object = self.repo:tree(entry.id)
|
||||
elseif entry.type == 'blob' then
|
||||
object = self.repo:blob(entry.id)
|
||||
object.mode = entry.mode
|
||||
elseif entry.type == 'commit' then
|
||||
-- this is possibly a commit in a submodule,
|
||||
-- do not retrieve it from current repo
|
||||
object = entry
|
||||
else
|
||||
error('Unknown entry type: ' .. entry.type)
|
||||
end
|
||||
return n, entry.type, object
|
||||
end
|
||||
end, self._entries
|
||||
end
|
||||
|
||||
function Tree:entry(n)
|
||||
local e = self._entries[n]
|
||||
if not e then return end
|
||||
if e.type == 'tree' then
|
||||
return self.repo:tree(e.id)
|
||||
elseif e.type == 'commit' then
|
||||
return self.repo:commit(e.id)
|
||||
elseif e.type == 'blob' then
|
||||
return self.repo:blob(e.id)
|
||||
else
|
||||
error('Unknown entry type: ' .. e.type)
|
||||
end
|
||||
end
|
||||
|
||||
function Tree:walk(func, path)
|
||||
path = path or '.'
|
||||
assert(type(func) == "function", "argument is not a function")
|
||||
local function walk(tree, path)
|
||||
for name, type, entry in tree:entries() do
|
||||
local entry_path = join_path(path, name)
|
||||
func(entry, entry_path, type)
|
||||
|
||||
if type == "tree" then
|
||||
walk(entry, entry_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
walk(self, path)
|
||||
end
|
||||
|
||||
function Tree:checkoutTo(path)
|
||||
util.make_dir(path)
|
||||
self:walk(function (entry, entry_path, type)
|
||||
if type == 'tree' then
|
||||
util.make_dir(entry_path)
|
||||
elseif type == 'blob' then
|
||||
local out = assert(io.open(entry_path, 'wb'))
|
||||
out:write(entry:content())
|
||||
out:close()
|
||||
if isPosix then
|
||||
local mode = entry.mode:sub(-3,-1) -- fixme: is this ok?
|
||||
local cmd = 'chmod '..mode..' "'..entry_path..'"'
|
||||
os.execute(cmd)
|
||||
end
|
||||
elseif type == 'commit' then
|
||||
-- this is a submodule referencing a commit,
|
||||
-- make a directory for it
|
||||
util.make_dir(entry_path)
|
||||
else
|
||||
error('Unknown entry type: ', type)
|
||||
end
|
||||
end, path)
|
||||
end
|
||||
|
||||
Blob = {}
|
||||
Blob.__index = Blob
|
||||
|
||||
function Blob:content()
|
||||
if self.stored then
|
||||
local f = self.repo:raw_object(self.id)
|
||||
local ret = f:read('*a') or ""
|
||||
f:close()
|
||||
return ret
|
||||
else
|
||||
return self.data
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,316 @@
|
||||
local io = io
|
||||
local core = require 'git.core'
|
||||
|
||||
local assert, pcall, print, select, setmetatable, string, type, unpack =
|
||||
assert, pcall, print, select, setmetatable, string, type, unpack
|
||||
|
||||
local ord = string.byte
|
||||
local fmt = string.format
|
||||
local concat, insert = table.concat, table.insert
|
||||
|
||||
local band = core.band
|
||||
local rshift, lshift = core.rshift, core.lshift
|
||||
|
||||
local to_hex = git.util.to_hex
|
||||
local from_hex = git.util.from_hex
|
||||
local object_sha = git.util.object_sha
|
||||
local binary_sha = git.util.binary_sha
|
||||
local readable_sha = git.util.readable_sha
|
||||
local tmpfile = git.util.tmpfile
|
||||
local reader = git.util.reader
|
||||
|
||||
module(...)
|
||||
|
||||
-- read git/Documentation/technical/pack-format.txt for some inspiration
|
||||
|
||||
-- 1 = commit, 2 = tree ...
|
||||
local types = {'commit', 'tree', 'blob', 'tag', '???', 'ofs_delta', 'ref_delta'}
|
||||
|
||||
-- read a 4 byte unsigned integer stored in network order
|
||||
local function read_int(f)
|
||||
local s = f:read(4)
|
||||
local a,b,c,d = s:byte(1,4)
|
||||
return a*256^3 + b*256^2 + c*256 + d
|
||||
end
|
||||
|
||||
-- read in the type and file length
|
||||
local function read_object_header(f)
|
||||
local b = ord(f:read(1))
|
||||
local type = band(rshift(b, 4), 0x7)
|
||||
local len = band(b, 0xF)
|
||||
local ofs = 0
|
||||
while band(b, 0x80) ~= 0 do
|
||||
b = ord(f:read(1))
|
||||
len = len + lshift(band(b, 0x7F), ofs * 7 + 4)
|
||||
ofs = ofs + 1
|
||||
end
|
||||
return len, type
|
||||
end
|
||||
|
||||
-- reads in the delta header and returns the offset where original data is stored
|
||||
local function read_delta_header(f)
|
||||
local b = ord(f:read(1))
|
||||
local offset = band(b, 0x7F)
|
||||
while band(b, 0x80) ~= 0 do
|
||||
offset = offset + 1
|
||||
b = ord(f:read(1))
|
||||
offset = lshift(offset, 7) + band(b, 0x7F)
|
||||
end
|
||||
return offset
|
||||
end
|
||||
|
||||
-- read just enough of file `f` to uncompress `size` bytes
|
||||
local function uncompress_by_len(f, size)
|
||||
local z = core.inflate()
|
||||
local chunks = {}
|
||||
local CHUNK_SIZE = 1024
|
||||
local curr_pos = f:seek()
|
||||
local inflated, eof, total
|
||||
-- read until end of zlib-compresed stream
|
||||
while not eof do
|
||||
local data = f:read(CHUNK_SIZE)
|
||||
inflated, eof, total = z(data)
|
||||
insert(chunks, inflated)
|
||||
end
|
||||
-- repair the current position in stream
|
||||
f:seek('set', curr_pos + total)
|
||||
return concat(chunks)
|
||||
end
|
||||
|
||||
-- uncompress the object from the current location in `f`
|
||||
local function unpack_object(f, len, type)
|
||||
local data = uncompress_by_len(f, len)
|
||||
return data, len, type
|
||||
end
|
||||
|
||||
-- returns a size value encoded in delta data
|
||||
local function delta_size(f)
|
||||
local size = 0
|
||||
local i = 0
|
||||
repeat
|
||||
local b = ord(f:read(1))
|
||||
size = size + lshift(band(b, 0x7F), i)
|
||||
i = i + 7
|
||||
until band(b, 0x80) == 0
|
||||
return size
|
||||
end
|
||||
|
||||
-- returns a patched object from string `base` according to `delta` data
|
||||
local function patch_object(base, delta, base_type)
|
||||
-- insert delta codes into temporary file
|
||||
local df = reader(delta)
|
||||
|
||||
-- retrieve original and result size (for checks)
|
||||
local orig_size = delta_size(df)
|
||||
assert(#base == orig_size, fmt('#base(%d) ~= orig_size(%d)', #base, orig_size))
|
||||
|
||||
local result_size = delta_size(df)
|
||||
local size = result_size
|
||||
|
||||
local result = {}
|
||||
|
||||
-- process the delta codes
|
||||
local cmd = df:read(1)
|
||||
while cmd do
|
||||
cmd = ord(cmd)
|
||||
if cmd == 0 then
|
||||
error('unexpected delta code 0')
|
||||
elseif band(cmd, 0x80) ~= 0 then -- copy a selected part of base data
|
||||
local cp_off, cp_size = 0, 0
|
||||
-- retrieve offset
|
||||
if band(cmd, 0x01) ~= 0 then cp_off = ord(df:read(1)) end
|
||||
if band(cmd, 0x02) ~= 0 then cp_off = cp_off + ord(df:read(1))*256 end
|
||||
if band(cmd, 0x04) ~= 0 then cp_off = cp_off + ord(df:read(1))*256^2 end
|
||||
if band(cmd, 0x08) ~= 0 then cp_off = cp_off + ord(df:read(1))*256^3 end
|
||||
-- retrieve size
|
||||
if band(cmd, 0x10) ~= 0 then cp_size = ord(df:read(1)) end
|
||||
if band(cmd, 0x20) ~= 0 then cp_size = cp_size + ord(df:read(1))*256 end
|
||||
if band(cmd, 0x40) ~= 0 then cp_size = cp_size + ord(df:read(1))*256^2 end
|
||||
if cp_size == 0 then cp_size = 0x10000 end
|
||||
if cp_off + cp_size > #base or cp_size > size then break end
|
||||
-- get the data and append it to result
|
||||
local data = base:sub(cp_off + 1, cp_off + cp_size)
|
||||
insert(result, data)
|
||||
size = size - cp_size
|
||||
else -- insert new data
|
||||
if cmd > size then break end
|
||||
local data = df:read(cmd)
|
||||
insert(result, data)
|
||||
size = size - cmd
|
||||
end
|
||||
cmd = df:read(1)
|
||||
end
|
||||
|
||||
df:close()
|
||||
|
||||
result = concat(result)
|
||||
assert(#result == result_size, fmt('#result(%d) ~= result_size(%d)', #result, result_size))
|
||||
return result, result_size, base_type
|
||||
end
|
||||
|
||||
Pack = {}
|
||||
Pack.__index = Pack
|
||||
|
||||
-- read an object from the current location in pack, or from a specific `offset`
|
||||
-- if specified
|
||||
function Pack:read_object(offset, ignore_data)
|
||||
local f = self.pack_file
|
||||
if offset then
|
||||
f:seek('set', offset)
|
||||
end
|
||||
local curr_pos = f:seek()
|
||||
|
||||
local len, type = read_object_header(f)
|
||||
if type < 5 then -- commit, tree, blob, tag
|
||||
return unpack_object(f, len, type)
|
||||
elseif type == 6 then -- ofs_delta
|
||||
local offset = read_delta_header(f)
|
||||
local delta_data = uncompress_by_len(f, len)
|
||||
if not ignore_data then
|
||||
-- the offset is negative from the current location
|
||||
local base, base_len, base_type = self:read_object(curr_pos - offset)
|
||||
return patch_object(base, delta_data, base_type)
|
||||
end
|
||||
elseif type == 7 then -- ref_delta
|
||||
local sha = f:read(20)
|
||||
local delta_data = uncompress_by_len(f, len)
|
||||
if not ignore_data then
|
||||
-- lookup the object in the pack by sha
|
||||
-- FIXME: maybe lookup in repo/other packs
|
||||
local base_offset = self.index[binary_sha(sha)]
|
||||
local base, base_len, base_type = self:read_object(base_offset)
|
||||
return patch_object(base, delta_data, base_type)
|
||||
end
|
||||
else
|
||||
error('unknown object type: '..type)
|
||||
end
|
||||
end
|
||||
|
||||
-- returns true if this pack contains the given object
|
||||
function Pack:has_object(sha)
|
||||
return self.index[binary_sha(sha)] ~= nil
|
||||
end
|
||||
|
||||
-- if the object name `sha` exists in the pack, returns a temporary file with the
|
||||
-- object content, length and type, otherwise returns nil
|
||||
function Pack:get_object(sha)
|
||||
local offset = self.index[binary_sha(sha)]
|
||||
if not offset then
|
||||
print('!!! Failed to find object', readable_sha(sha))
|
||||
end
|
||||
|
||||
local data, len, type = self:read_object(offset)
|
||||
print(readable_sha(sha), len, type, data)
|
||||
local f = tmpfile()
|
||||
f:write(data)
|
||||
f:seek('set', 0)
|
||||
|
||||
return f, len, types[type]
|
||||
end
|
||||
|
||||
function Pack:unpack(repo)
|
||||
for i=1, self.nobjects do
|
||||
local offset = self.offsets[i]
|
||||
local data, len, type = self:read_object(offset)
|
||||
repo:store_object(data, len, types[type])
|
||||
end
|
||||
end
|
||||
|
||||
-- parses the index
|
||||
function Pack:parse_index(index_file)
|
||||
local f = index_file
|
||||
|
||||
local head = f:read(4)
|
||||
assert(head == '\255tOc', "Incorrect header: " .. head)
|
||||
local version = read_int(f)
|
||||
assert(version == 2, "Incorrect version: " .. version)
|
||||
|
||||
-- first the fanout table (how many objects are in the index, whose
|
||||
-- first byte is below or equal to i)
|
||||
local fanout = {}
|
||||
for i=0, 255 do
|
||||
local nobjs = read_int(f)
|
||||
fanout[i] = nobjs
|
||||
end
|
||||
|
||||
-- the last element in fanout is the number of all objects in index
|
||||
local count = fanout[255]
|
||||
|
||||
-- then come the sorted object names (=sha hash)
|
||||
local tmp = {}
|
||||
for i=1,count do
|
||||
local sha = f:read(20)
|
||||
tmp[i] = { sha = sha }
|
||||
end
|
||||
|
||||
-- then the CRCs (assume ok, skip them)
|
||||
for i=1, count do
|
||||
local crc = f:read(4)
|
||||
end
|
||||
|
||||
-- then come the offsets - read just the 32bit ones, does not handle packs > 2G
|
||||
for i=1, count do
|
||||
local offset = read_int(f)
|
||||
tmp[i].offset = offset
|
||||
end
|
||||
|
||||
-- construct the lookup table
|
||||
local lookup = {}
|
||||
for i=1, count do
|
||||
lookup[tmp[i].sha] = tmp[i].offset
|
||||
end
|
||||
self.index = lookup
|
||||
end
|
||||
|
||||
-- constructs the index/offsets if the index file is missing
|
||||
function Pack:construct_index(path)
|
||||
local index = {}
|
||||
for i=1, self.nobjects do
|
||||
local offset = self.offsets[i]
|
||||
local data, len, type = self:read_object(offset)
|
||||
local sha = object_sha(data, len, types[type])
|
||||
index[binary_sha(sha)] = offset
|
||||
end
|
||||
self.index = index
|
||||
end
|
||||
|
||||
function Pack:close()
|
||||
self.pack_file:close()
|
||||
end
|
||||
|
||||
function Pack.open(path)
|
||||
local fp = assert(io.open(path, 'rb')) -- stays open
|
||||
|
||||
-- read the pack header
|
||||
local head = fp:read(4)
|
||||
assert(head == 'PACK', "Incorrect header: " .. head)
|
||||
local version = read_int(fp)
|
||||
assert(version == 2, "Incorrect version: " .. version)
|
||||
local nobj = read_int(fp)
|
||||
|
||||
local pack = setmetatable({
|
||||
offsets = {},
|
||||
nobjects = nobj,
|
||||
pack_file = fp,
|
||||
}, Pack)
|
||||
|
||||
-- fill the offsets by traversing through the pack
|
||||
for i=1,nobj do
|
||||
pack.offsets[i] = fp:seek()
|
||||
-- ignore the object data, we only need the offset in the pack
|
||||
pack:read_object(nil, true)
|
||||
end
|
||||
|
||||
-- read the index
|
||||
local fi = io.open((path:gsub('%.pack$', '.idx')), 'rb')
|
||||
if fi then
|
||||
pack:parse_index(fi)
|
||||
fi:close()
|
||||
else
|
||||
pack:construct_index(path)
|
||||
end
|
||||
|
||||
return pack
|
||||
end
|
||||
|
||||
return Pack
|
@ -0,0 +1,188 @@
|
||||
local socket = require 'socket'
|
||||
local urllib = require 'socket.url'
|
||||
local lfs = require 'lfs'
|
||||
|
||||
local Repo = git.repo.Repo
|
||||
local Pack = git.pack.Pack
|
||||
local join_path = git.util.join_path
|
||||
local parent_dir = git.util.parent_dir
|
||||
local make_dir = git.util.make_dir
|
||||
local correct_separators = git.util.correct_separators
|
||||
|
||||
local assert, error, getmetatable, io, os, pairs, print, require, string, tonumber =
|
||||
assert, error, getmetatable, io, os, pairs, print, require, string, tonumber
|
||||
|
||||
local _VERSION, newproxy = _VERSION, newproxy
|
||||
|
||||
module(...)
|
||||
|
||||
local GIT_PORT = 9418
|
||||
|
||||
local function git_connect(host)
|
||||
local sock = assert(socket.connect(host, GIT_PORT))
|
||||
local gitsocket = {}
|
||||
|
||||
function gitsocket:send(data)
|
||||
if not data then -- flush packet
|
||||
sock:send('0000')
|
||||
else
|
||||
local len = #data + 4
|
||||
len = string.format("%04x", len)
|
||||
assert(sock:send(len .. data))
|
||||
end
|
||||
end
|
||||
|
||||
function gitsocket:receive()
|
||||
local len = assert(sock:receive(4))
|
||||
len = tonumber(len, 16)
|
||||
if len == 0 then return end -- flush packet
|
||||
local data = assert(sock:receive(len - 4))
|
||||
return data
|
||||
end
|
||||
|
||||
function gitsocket:close()
|
||||
sock:close()
|
||||
end
|
||||
|
||||
return gitsocket
|
||||
end
|
||||
|
||||
local function addFinalizer(object, finalizer)
|
||||
if _VERSION <= "Lua 5.1" then
|
||||
local gc = newproxy(true)
|
||||
getmetatable(gc).__gc = finalizer
|
||||
object.__gc = gc
|
||||
else
|
||||
local mt = getmetatable(object)
|
||||
if mt then mt.__gc = finalizer
|
||||
else setmetatable(object, {__gc = finalizer})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function git_fetch(host, path, repo, head, supress_progress)
|
||||
local s = git_connect(host)
|
||||
s:send('git-upload-pack '..path..'\0host='..host..'\0')
|
||||
|
||||
local refs, refsbyname = {}, {}
|
||||
repeat
|
||||
local ref = s:receive()
|
||||
if ref then
|
||||
local sha, name = ref:sub(1,40), ref:sub(42, -2)
|
||||
refs[sha] = name
|
||||
refsbyname[name] = sha
|
||||
end
|
||||
until not ref
|
||||
|
||||
local wantedSha
|
||||
local headsha = head and refsbyname[head]
|
||||
|
||||
for sha, ref in pairs(refs) do
|
||||
-- we implicitly want this ref
|
||||
local wantObject = true
|
||||
-- unless we ask for a specific head
|
||||
if headsha then
|
||||
if sha ~= headsha then
|
||||
wantObject = false
|
||||
else
|
||||
wantedSha = sha
|
||||
end
|
||||
end
|
||||
-- or we already have it
|
||||
if repo and repo:has_object(sha) then
|
||||
wantObject = false
|
||||
end
|
||||
if wantObject then
|
||||
s:send('want '..sha..' multi_ack_detailed side-band-64k ofs-delta\n')
|
||||
end
|
||||
end
|
||||
|
||||
if head and not wantedSha then
|
||||
error("Server does not have "..head)
|
||||
end
|
||||
|
||||
s:send('deepen 1')
|
||||
s:send()
|
||||
while s:receive() do end
|
||||
s:send('done\n')
|
||||
|
||||
assert(s:receive() == "NAK\n")
|
||||
|
||||
local packname = os.tmpname() .. '.pack'
|
||||
local packfile = assert(io.open(packname, 'wb'))
|
||||
repeat
|
||||
local got = s:receive()
|
||||
if got then
|
||||
-- get sideband channel, 1=pack data, 2=progress, 3=error
|
||||
local cmd = string.byte(got:sub(1,1))
|
||||
local data = got:sub(2)
|
||||
if cmd == 1 then
|
||||
packfile:write(data)
|
||||
elseif cmd == 2 then
|
||||
if not supress_progress then io.write(data) end
|
||||
else
|
||||
error(data)
|
||||
end
|
||||
end
|
||||
until not got
|
||||
|
||||
packfile:close()
|
||||
s:close()
|
||||
|
||||
local pack = Pack.open(packname)
|
||||
if repo then
|
||||
pack:unpack(repo)
|
||||
repo.isShallow = true
|
||||
if wantedSha then
|
||||
local headfile = correct_separators(join_path(repo.dir, head))
|
||||
assert(make_dir(parent_dir(headfile)))
|
||||
local f = assert(io.open(headfile, 'wb'))
|
||||
f:write(wantedSha)
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
addFinalizer(pack, function()
|
||||
os.remove(packname)
|
||||
end)
|
||||
|
||||
return pack, wantedSha
|
||||
end
|
||||
|
||||
function fetch(url, repo, head, supress_progress)
|
||||
if repo then assert(getmetatable(repo) == Repo, "arg #2 is not a repository") end
|
||||
url = urllib.parse(url)
|
||||
if url.scheme == 'git' then
|
||||
local pack, sha = git_fetch(url.host, url.path, repo, head, supress_progress)
|
||||
return pack, sha
|
||||
else
|
||||
error('unsupported scheme: '..url.scheme)
|
||||
end
|
||||
end
|
||||
|
||||
function remotes(url)
|
||||
-- TODO: refactor common code
|
||||
url = assert(urllib.parse(url))
|
||||
|
||||
if url.scheme ~= 'git' then
|
||||
error('unsupported scheme: '..url.scheme)
|
||||
end
|
||||
|
||||
local host, path = url.host, url.path
|
||||
|
||||
local s = git_connect(host)
|
||||
s:send('git-upload-pack '..path..'\0host='..host..'\0')
|
||||
|
||||
local remote = {}
|
||||
repeat
|
||||
local ref = s:receive()
|
||||
if ref then
|
||||
local sha, name = ref:sub(1,40), ref:sub(42, -2)
|
||||
remote[name] = sha
|
||||
end
|
||||
until not ref
|
||||
|
||||
s:close()
|
||||
|
||||
return remote
|
||||
end
|
@ -0,0 +1,283 @@
|
||||
local util = require 'git.util'
|
||||
local objects = require 'git.objects'
|
||||
local core = require 'git.core'
|
||||
local pack = require 'git.pack'
|
||||
|
||||
local join_path = util.join_path
|
||||
local decompressed = util.decompressed
|
||||
local read_until_nul = util.read_until_nul
|
||||
local to_hex = util.to_hex
|
||||
local object_sha = util.object_sha
|
||||
local readable_sha = util.readable_sha
|
||||
|
||||
local deflate = core.deflate
|
||||
|
||||
local lfs = require 'lfs'
|
||||
local assert, error, io, ipairs, print, os, setmetatable, string, table =
|
||||
assert, error, io, ipairs, print, os, setmetatable, string, table
|
||||
|
||||
module(...)
|
||||
|
||||
Repo = {}
|
||||
Repo.__index = Repo
|
||||
|
||||
-- retrieves an object identified by `sha` from the repository or its packs
|
||||
-- returns a file-like object (supports 'read', 'seek' and 'close'), the size
|
||||
-- of the object and its type
|
||||
-- errors when the object does not exist
|
||||
function Repo:raw_object(sha)
|
||||
-- first, look in 'objects' directory
|
||||
-- first byte of sha is the directory, the rest is name of object file
|
||||
sha = readable_sha(sha)
|
||||
local dir = sha:sub(1,2)
|
||||
local file = sha:sub(3)
|
||||
local path = join_path(self.dir, 'objects', dir, file)
|
||||
|
||||
if not lfs.attributes(path, 'size') then
|
||||
-- then, try to look in packs
|
||||
for _, pack in ipairs(self.packs) do
|
||||
local obj, len, typ = pack:get_object(sha)
|
||||
if obj then
|
||||
return obj, len, typ
|
||||
end
|
||||
end
|
||||
error('Object not found in object neither in packs: '..sha)
|
||||
else
|
||||
-- the objects are zlib compressed
|
||||
local f = decompressed(path)
|
||||
|
||||
-- retrieve the type and length - <type> SP <len> \0 <data...>
|
||||
local content = read_until_nul(f)
|
||||
local typ, len = content:match('(%w+) (%d+)')
|
||||
|
||||
return f, len, typ
|
||||
end
|
||||
end
|
||||
|
||||
--- Store a new object into the repository in `objects` directory.
|
||||
-- @param data A string containing the contents of the new file.
|
||||
-- @param len The length of the data.
|
||||
-- @param type One of 'commit', 'blob', 'tree', 'tag'
|
||||
function Repo:store_object(data, len, type)
|
||||
local sha = readable_sha(object_sha(data, len, type))
|
||||
local dir = sha:sub(1,2)
|
||||
local file = sha:sub(3)
|
||||
util.make_dir(join_path(self.dir, 'objects', dir))
|
||||
local path = join_path(self.dir, 'objects', dir, file)
|
||||
local fo = assert(io.open(path, 'wb'))
|
||||
local header = type .. ' ' .. len .. '\0'
|
||||
local compressed = deflate()(header .. data, "finish")
|
||||
fo:write(compressed)
|
||||
fo:close()
|
||||
end
|
||||
|
||||
local function resolvetag(f)
|
||||
local tag
|
||||
local line = f:read()
|
||||
while line do
|
||||
tag = line:match('^object (%x+)$')
|
||||
if tag then break end
|
||||
line = f:read()
|
||||
end
|
||||
f:close()
|
||||
return tag
|
||||
end
|
||||
|
||||
function Repo:commit(sha)
|
||||
local f, len, typ = self:raw_object(sha)
|
||||
while typ == 'tag' do
|
||||
sha = assert(resolvetag(f), 'could not parse tag for '..readable_sha(sha))
|
||||
f, len, typ = self:raw_object(sha)
|
||||
end
|
||||
assert(typ == 'commit', string.format('%s (%s) is not a commit', sha, typ))
|
||||
|
||||
local commit = { id = sha, repo = self, stored = true, parents = {} }
|
||||
repeat
|
||||
local line = f:read()
|
||||
if not line then break end
|
||||
|
||||
local space = line:find(' ') or 0
|
||||
local word = line:sub(1, space - 1)
|
||||
local afterSpace = line:sub(space + 1)
|
||||
|
||||
if word == 'tree' then
|
||||
commit.tree_sha = afterSpace
|
||||
elseif word == 'parent' then
|
||||
table.insert(commit.parents, afterSpace)
|
||||
elseif word == 'author' then
|
||||
commit.author = afterSpace
|
||||
elseif word == 'committer' then
|
||||
commit.committer = afterSpace
|
||||
elseif commit.message then
|
||||
table.insert(commit.message, line)
|
||||
elseif line == '' then
|
||||
commit.message = {}
|
||||
end
|
||||
until false -- ends with break
|
||||
f:close()
|
||||
|
||||
commit.message = table.concat(commit.message, '\n')
|
||||
|
||||
return setmetatable(commit, objects.Commit)
|
||||
end
|
||||
|
||||
function Repo:tree(sha)
|
||||
local f, len, typ = self:raw_object(sha)
|
||||
assert(typ == 'tree', string.format('%s (%s) is not a tree', sha, typ))
|
||||
|
||||
local tree = { id = sha, repo = self, stored = true, _entries = {} }
|
||||
|
||||
while true do
|
||||
local info = read_until_nul(f)
|
||||
if not info then break end
|
||||
local entry_sha = to_hex(f:read(20))
|
||||
local mode, name = info:match('^(%d+)%s(.+)$')
|
||||
local entry_type = 'blob'
|
||||
if mode == '40000' then
|
||||
entry_type = 'tree'
|
||||
elseif mode == '160000' then
|
||||
entry_type = 'commit'
|
||||
end
|
||||
tree._entries[name] = { mode = mode, id = entry_sha, type = entry_type }
|
||||
end
|
||||
|
||||
f:close()
|
||||
|
||||
return setmetatable(tree, objects.Tree)
|
||||
end
|
||||
|
||||
-- retrieves a Blob
|
||||
function Repo:blob(sha)
|
||||
local f, len, typ = self:raw_object(sha)
|
||||
f:close() -- can be reopened in Blob:content()
|
||||
|
||||
assert(typ == 'blob', string.format('%s (%s) is not a blob', sha, typ))
|
||||
return setmetatable({
|
||||
id = sha,
|
||||
len = len,
|
||||
repo = self,
|
||||
stored = true }, objects.Blob)
|
||||
end
|
||||
|
||||
function Repo:head()
|
||||
return self:commit(self.refs.HEAD)
|
||||
end
|
||||
|
||||
function Repo:has_object(sha)
|
||||
local dir = sha:sub(1,2)
|
||||
local file = sha:sub(3)
|
||||
local path = join_path(self.dir, 'objects', dir, file)
|
||||
|
||||
if lfs.attributes(path, 'size') then return true end
|
||||
|
||||
for _, pack in ipairs(self.packs) do
|
||||
local has = pack:has_object(sha)
|
||||
if has then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Repo:checkout(sha, target)
|
||||
if not target then target = self.workDir end
|
||||
assert(target, 'target directory not specified')
|
||||
|
||||
local commit = self:commit(sha)
|
||||
commit:checkout(target)
|
||||
|
||||
-- if the repo was checked out using the deepen command (one level of history only)
|
||||
-- mark the commit's parent as shalow, that is it has no history
|
||||
if self.isShallow then
|
||||
-- if it has a parent, mark it shallow
|
||||
if commit.parents[1] then
|
||||
local f = assert(io.open(self.dir .. '/shallow', "w"))
|
||||
f:write(commit.parents[1], '\n')
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Repo:close()
|
||||
for _, pack in ipairs(self.packs) do
|
||||
pack:close()
|
||||
end
|
||||
end
|
||||
|
||||
function create(dir)
|
||||
if not dir:match('%.git.?$') then
|
||||
dir = join_path(dir, '.git')
|
||||
end
|
||||
|
||||
util.make_dir(dir)
|
||||
util.make_dir(dir .. '/branches')
|
||||
util.make_dir(dir .. '/hooks')
|
||||
util.make_dir(dir .. '/info')
|
||||
util.make_dir(dir .. '/objects/info')
|
||||
util.make_dir(dir .. '/objects/pack')
|
||||
util.make_dir(dir .. '/refs/heads')
|
||||
util.make_dir(dir .. '/refs/tags')
|
||||
util.make_dir(dir .. '/refs/remotes')
|
||||
|
||||
do
|
||||
local f = assert(io.open(dir .. "/HEAD", "w"))
|
||||
f:write("ref: refs/heads/master\n")
|
||||
f:close()
|
||||
end
|
||||
|
||||
local refs = {}
|
||||
local packs = {}
|
||||
|
||||
return setmetatable({
|
||||
dir = dir,
|
||||
refs = refs,
|
||||
packs = packs,
|
||||
}, Repo)
|
||||
end
|
||||
|
||||
-- opens a repository located in working directory `dir` or directly a .git repo
|
||||
function open(dir)
|
||||
local workDir = dir
|
||||
if not dir:match('%.git.?$') then
|
||||
dir = join_path(dir, '.git')
|
||||
else
|
||||
workDir = nil -- no working directory, working directly with repo
|
||||
end
|
||||
|
||||
local refs = {}
|
||||
for _,d in ipairs{'refs/heads', 'refs/tags'} do
|
||||
for fn in lfs.dir(join_path(dir, d)) do
|
||||
if fn ~= '.' and fn ~= '..' then
|
||||
local path = join_path(dir, d, fn)
|
||||
local f = assert(io.open(path), 'rb')
|
||||
local ref = f:read()
|
||||
refs[join_path(d, fn)] = ref
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local packs = {}
|
||||
for fn in lfs.dir(join_path(dir, 'objects/pack')) do
|
||||
if fn:match('%.pack$') then
|
||||
local path = join_path(dir, 'objects/pack', fn)
|
||||
table.insert(packs, pack.open(path))
|
||||
end
|
||||
end
|
||||
|
||||
local head = io.open(join_path(dir, 'HEAD'), 'rb')
|
||||
if head then
|
||||
local src = head:read()
|
||||
local HEAD = src:match('ref: (.-)$')
|
||||
refs.HEAD = refs[HEAD]
|
||||
head:close()
|
||||
end
|
||||
|
||||
return setmetatable({
|
||||
dir = dir,
|
||||
workDir = workDir,
|
||||
refs = refs,
|
||||
packs = packs,
|
||||
}, Repo)
|
||||
end
|
||||
|
||||
return Repo
|
@ -0,0 +1,233 @@
|
||||
local lfs = require 'lfs'
|
||||
local core = require 'git.core'
|
||||
local deflate = core.deflate
|
||||
local inflate = core.inflate
|
||||
local sha = core.sha
|
||||
|
||||
module(..., package.seeall)
|
||||
|
||||
local BUF_SIZE = 4096
|
||||
|
||||
local dirsep = package.config:sub(1,1)
|
||||
|
||||
-- replaces '/' path separators on Windows with the correct ones ('\\')
|
||||
function correct_separators(path)
|
||||
return path:gsub('/', dirsep)
|
||||
end
|
||||
|
||||
-- joins several path components into a single path, uses system-specific directory
|
||||
-- separator, cleans input, i.e. join_path('a/', 'b', 'c/') => 'a/b/c'
|
||||
function join_path(...)
|
||||
local n = select('#', ...)
|
||||
local args = {...}
|
||||
for i=1,n do
|
||||
args[i] = args[i]:gsub(dirsep..'?$', '')
|
||||
end
|
||||
return table.concat(args, dirsep, 1, n)
|
||||
end
|
||||
|
||||
-- Return the path with the all occurences of '/.' or '\.' (representing
|
||||
-- the current directory) removed.
|
||||
local function remove_curr_dir_dots(path)
|
||||
while path:match(dirsep .. "%." .. dirsep) do -- match("/%./")
|
||||
path = path:gsub(dirsep .. "%." .. dirsep, dirsep) -- gsub("/%./", "/")
|
||||
end
|
||||
return path:gsub(dirsep .. "%.$", "") -- gsub("/%.$", "")
|
||||
end
|
||||
|
||||
-- Return whether the path is a root.
|
||||
local function is_root(path)
|
||||
return path:find("^[%u%U.]?:?[/\\]$")
|
||||
end
|
||||
|
||||
-- Return the path with the unnecessary trailing separator removed.
|
||||
local function remove_trailing(path)
|
||||
if path:sub(-1) == dirsep and not is_root(path) then path = path:sub(1,-2) end
|
||||
return path
|
||||
end
|
||||
|
||||
-- Extract file or directory name from its path.
|
||||
local function extract_name(path)
|
||||
if is_root(path) then return path end
|
||||
|
||||
path = remove_trailing(path)
|
||||
path = path:gsub("^.*" .. dirsep, "")
|
||||
return path
|
||||
end
|
||||
|
||||
-- Return the string 'str', with all magic (pattern) characters escaped.
|
||||
local function escape_magic(str)
|
||||
local escaped = str:gsub('[%-%.%+%[%]%(%)%^%%%?%*%^%$]','%%%1')
|
||||
return escaped
|
||||
end
|
||||
|
||||
-- Return parent directory of the 'path' or nil if there's no parent directory.
|
||||
-- If 'path' is a path to file, return the directory the file is in.
|
||||
function parent_dir(path)
|
||||
path = remove_curr_dir_dots(path)
|
||||
path = remove_trailing(path)
|
||||
|
||||
local dir = path:gsub(escape_magic(extract_name(path)) .. "$", "")
|
||||
if dir == "" then
|
||||
return nil
|
||||
else
|
||||
return remove_trailing(dir)
|
||||
end
|
||||
end
|
||||
|
||||
-- Make a new directory, making also all of its parent directories that doesn't exist.
|
||||
function make_dir(path)
|
||||
if lfs.attributes(path) then
|
||||
return true
|
||||
else
|
||||
local par_dir = parent_dir(path)
|
||||
if par_dir then
|
||||
assert(make_dir(par_dir))
|
||||
end
|
||||
return lfs.mkdir(path)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Reader class
|
||||
-- adapted from Penlight: https://raw.github.com/stevedonovan/Penlight/master/lua/pl/stringio.lua
|
||||
|
||||
local SR = {}
|
||||
SR.__index = SR
|
||||
|
||||
function SR:_read(fmt)
|
||||
local i,str = self.i,self.str
|
||||
local sz = #str
|
||||
if i > sz then return nil, "past end of file" end
|
||||
local res
|
||||
if fmt == '*l' or fmt == '*L' then
|
||||
local idx = str:find('\n',i) or (sz+1)
|
||||
res = str:sub(i,fmt == '*l' and idx-1 or idx)
|
||||
self.i = idx+1
|
||||
elseif fmt == '*a' then
|
||||
res = str:sub(i)
|
||||
self.i = sz+1
|
||||
elseif fmt == '*n' then
|
||||
local _,i2,i2,idx
|
||||
_,idx = str:find ('%s*%d+',i)
|
||||
_,i2 = str:find ('^%.%d+',idx+1)
|
||||
if i2 then idx = i2 end
|
||||
_,i2 = str:find ('^[eE][%+%-]*%d+',idx+1)
|
||||
if i2 then idx = i2 end
|
||||
local val = str:sub(i,idx)
|
||||
res = tonumber(val)
|
||||
self.i = idx+1
|
||||
elseif type(fmt) == 'number' then
|
||||
res = str:sub(i,i+fmt-1)
|
||||
self.i = i + fmt
|
||||
else
|
||||
error("bad read format",2)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function SR:read(...)
|
||||
if select('#',...) == 0 then
|
||||
return self:_read('*l')
|
||||
else
|
||||
local res, fmts = {},{...}
|
||||
for i = 1, #fmts do
|
||||
res[i] = self:_read(fmts[i])
|
||||
end
|
||||
return unpack(res)
|
||||
end
|
||||
end
|
||||
|
||||
function SR:seek(whence,offset)
|
||||
local base
|
||||
whence = whence or 'cur'
|
||||
offset = offset or 0
|
||||
if whence == 'set' then
|
||||
base = 1
|
||||
elseif whence == 'cur' then
|
||||
base = self.i
|
||||
elseif whence == 'end' then
|
||||
base = #self.str
|
||||
end
|
||||
self.i = base + offset
|
||||
return self.i
|
||||
end
|
||||
|
||||
function SR:close() -- for compatibility only
|
||||
end
|
||||
|
||||
--- create a file-like object for reading from a given string.
|
||||
-- @param s The input string.
|
||||
function reader(s)
|
||||
return setmetatable({str=s,i=1},SR)
|
||||
end
|
||||
|
||||
|
||||
-- decompress the file and return a handle to temporary uncompressed file
|
||||
function decompressed(path)
|
||||
local fi = assert(io.open(path, 'rb'))
|
||||
local result = {}
|
||||
|
||||
local z = inflate()
|
||||
repeat
|
||||
local str = fi:read(BUF_SIZE)
|
||||
local data = z(str)
|
||||
if type(data) == 'string' then
|
||||
result[#result+1] = data
|
||||
else print('!!!', data) end
|
||||
until not str
|
||||
fi:close()
|
||||
|
||||
return reader(table.concat(result))
|
||||
end
|
||||
|
||||
-- reads until the byte \0, consumes it and returns the string up to the \0
|
||||
function read_until_nul(f)
|
||||
local t = {}
|
||||
repeat
|
||||
local c = f:read(1)
|
||||
if c and c ~= '\0' then t[#t+1] = c end
|
||||
until not c or c == '\0'
|
||||
if #t > 0 then
|
||||
return table.concat(t)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- converts a string to lowercase hex
|
||||
function to_hex(s)
|
||||
return (s:gsub('.', function(c)
|
||||
return string.format('%02x', string.byte(c))
|
||||
end))
|
||||
end
|
||||
|
||||
-- converts a string from hex to binary
|
||||
function from_hex(s)
|
||||
return (s:gsub('..', function(cc)
|
||||
return string.char(tonumber(cc, 16))
|
||||
end))
|
||||
end
|
||||
|
||||
-- always returns readable (hex) hash
|
||||
function readable_sha(s)
|
||||
if #s ~= 40 then return to_hex(s)
|
||||
else return s end
|
||||
end
|
||||
|
||||
-- always returns binary hash
|
||||
function binary_sha(s)
|
||||
if #s ~= 20 then return from_hex(s)
|
||||
else return s end
|
||||
end
|
||||
|
||||
function object_sha(data, len, type)
|
||||
local header = type .. ' ' .. len .. '\0'
|
||||
local res = sha(header .. data)
|
||||
return res
|
||||
end
|
||||
|
||||
function deflate(data)
|
||||
local c = deflate()
|
||||
return c(data, "finish")
|
||||
end
|
Reference in New Issue
Block a user