local replaceLine = true -- Setting to false may result in corrupted output local preload = type(package) == "table" and type(package.preload) == "table" and package.preload or {} local require = require if type(require) ~= "function" then local loading = {} local loaded = {} require = function(name) local result = loaded[name] if result ~= nil then if result == loading then error("loop or previous error loading module '" .. name .. "'", 2) end return result end loaded[name] = loading local contents = preload[name] if contents then result = contents(name) else error("cannot load '" .. name .. "'", 2) end if result == nil then result = true end loaded[name] = result return result end end preload["objects"] = function(...) local inflate_zlib = require "deflate".inflate_zlib local sha = require "metis.crypto.sha1" local band, bor, lshift, rshift = bit32.band, bit32.bor, bit32.lshift, bit32.rshift local byte, format, sub = string.byte, string.format, string.sub local types = { [0] = "none", "commit", "tree", "blob", "tag", nil, "ofs_delta", "ref_delta", "any", "max" } --- Get the type of a specific object -- @tparam Object x The object to get the type of -- @treturn string The object's type. local function get_type(x) return types[x.ty] or "?" end local event = ("luagit-%08x"):format(math.random(0, 2^24)) local function check_in() os.queueEvent(event) os.pullEvent(event) end local sha_format = ("%02x"):rep(20) local function reader(str) local expected_checksum = format(sha_format, byte(str, -20, -1)) local actual_checksum = sha(str:sub(1, -21)); if expected_checksum ~= actual_checksum then error(("checksum mismatch: expected %s, got %s"):format(expected_checksum, actual_checksum)) end str = str:sub(1, -20) local pos = 1 local function consume_read(len) if len <= 0 then error("len < 0", 2) end if pos > #str then error("end of stream") end local cur_pos = pos pos = pos + len local res = sub(str, cur_pos, pos - 1) if #res ~= len then error("expected " .. len .. " bytes, got" .. #res) end return res end local function read8() if pos > #str then error("end of stream") end local cur_pos = pos pos = pos + 1 return byte(str, cur_pos) end return { offset = function() return pos - 1 end, read8 = read8, read16 = function() return (read8() * (2^8)) + read8() end, read32 = function() return (read8() * (2^24)) + (read8() * (2^16)) + (read8() * (2^8)) + read8() end, read = consume_read, close = function() if pos ~= #str then error(("%d of %d bytes remaining"):format(#str - pos + 1, #str)) end end, } end --- Consume a string from the given input buffer -- -- @tparam Reader handle The handle to read from -- @tparam number size The number of decompressed bytes to read -- @treturn string The decompressed data local function get_data(handle, size) local tbl, n = {}, 1 inflate_zlib { input = handle.read8, output = function(x) tbl[n], n = string.char(x), n + 1 end } local res = table.concat(tbl) if #res ~= size then error(("expected %d decompressed bytes, got %d"):format(size, #res)) end return res end --- Decode a binary delta file, applying it to the original -- -- The format is described in more detail in [the Git documentation][git_pack] -- -- [git_pack]: https://git-scm.com/docs/pack-format#_deltified_representation -- -- @tparam string original The original string -- @tparam string delta The binary delta -- @treturn string The patched string local function apply_delta(original, delta) local delta_offset = 1 local function read_size() local c = byte(delta, delta_offset) delta_offset = delta_offset + 1 local size = band(c, 0x7f) local shift = 7 while band(c, 0x80) ~= 0 do c, delta_offset = byte(delta, delta_offset), delta_offset + 1 size, shift = size + lshift(band(c, 0x7f), shift), shift + 7 end return size end local original_length = read_size() local patched_length = read_size() if original_length ~= #original then error(("expected original of size %d, got size %d"):format(original_length, #original)) end local parts, n = {}, 1 while delta_offset <= #delta do local b = byte(delta, delta_offset) delta_offset = delta_offset + 1 if band(b, 0x80) ~= 0 then -- Copy from the original file. Each bit represents which optional length/offset -- bits are used. local offset, length = 0, 0 if band(b, 0x01) ~= 0 then offset, delta_offset = bor(offset, byte(delta, delta_offset)), delta_offset + 1 end if band(b, 0x02) ~= 0 then offset, delta_offset = bor(offset, lshift(byte(delta, delta_offset), 8)), delta_offset + 1 end if band(b, 0x04) ~= 0 then offset, delta_offset = bor(offset, lshift(byte(delta, delta_offset), 16)), delta_offset + 1 end if band(b, 0x08) ~= 0 then offset, delta_offset = bor(offset, lshift(byte(delta, delta_offset), 24)), delta_offset + 1 end if band(b, 0x10) ~= 0 then length, delta_offset = bor(length, byte(delta, delta_offset)), delta_offset + 1 end if band(b, 0x20) ~= 0 then length, delta_offset = bor(length, lshift(byte(delta, delta_offset), 8)), delta_offset + 1 end if band(b, 0x40) ~= 0 then length, delta_offset = bor(length, lshift(byte(delta, delta_offset), 16)), delta_offset + 1 end if length == 0 then length = 0x10000 end parts[n], n = sub(original, offset + 1, offset + length), n + 1 elseif b > 0 then -- Copy from the delta. The opcode encodes the length parts[n], n = sub(delta, delta_offset, delta_offset + b - 1), n + 1 delta_offset = delta_offset + b else error(("unknown opcode '%02x'"):format(b)) end end local patched = table.concat(parts) if patched_length ~= #patched then error(("expected patched of size %d, got size %d"):format(patched_length, #patched)) end return patched end --- Unpack a single object, populating the output table -- -- @tparam Reader handle The handle to read from -- @tparam { [string] = Object } out The populated data local function unpack_object(handle, out) local c = handle.read8() local ty = band(rshift(c, 4), 7) local size = band(c, 15) local shift = 4 while band(c, 0x80) ~= 0 do c = handle.read8() size = size + lshift(band(c, 0x7f), shift) shift = shift + 7 end local data if ty >= 1 and ty <= 4 then -- commit/tree/blob/tag data = get_data(handle, size) elseif ty == 6 then -- ofs_delta data = get_data(handle, size) error("ofs_delta not yet implemented") elseif ty == 7 then -- ref_delta local base_hash = sha_format:format(handle.read(20):byte(1, 20)) local delta = get_data(handle, size) local original = out[base_hash] if not original then error(("cannot find object %d to apply diff"):format(base_hash)) return end ty = original.ty data = apply_delta(original.data, delta) else error(("unknown object of type '%d'"):format(ty)) end -- We've got to do these separately. Format doesn't like null bytes local whole = ("%s %d\0"):format(types[ty], #data) .. data local sha = sha(whole) out[sha] = { ty = ty, data = data, sha = sha } end local function unpack(handle, progress) local header = handle.read(4) if header ~= "PACK" then error("expected PACK, got " .. header, 0) end local version = handle.read32() local entries = handle.read32() local out = {} for i = 1, entries do if progress then progress(i, entries) end check_in() unpack_object(handle, out) end return out end local function build_tree(objects, object, prefix, out) if not prefix then prefix = "" end if not out then out = {} end local idx = 1 while idx <= #object do -- dddddd NAME\0 local _, endidx, mode, name = object:find("^(%x+) ([^%z]+)%z", idx) if not endidx then break end name = prefix .. name local sha = object:sub(endidx + 1, endidx + 20):gsub(".", function(x) return ("%02x"):format(string.byte(x)) end) local entry = objects[sha] if not entry then error(("cannot find %s %s (%s)"):format(mode, name, sha)) end if entry.ty == 3 then out[name] = entry.data elseif entry.ty == 2 then build_tree(objects, entry.data, name .. "/", out) else error("unknown type for " .. name .. " (" .. sha .. "): " .. get_type(entry)) end idx = endidx + 21 end return out end local function build_commit(objects, sha) local commit = objects[sha] if not commit then error("cannot find commit " .. sha) end if commit.ty ~= 1 then error("Expected commit, got " .. types[commit.ty]) end local tree_sha = commit.data:match("tree (%x+)\n") if not tree_sha then error("Cannot find tree from commit") end local tree = objects[tree_sha] if not tree then error("cannot find tree " .. tree_sha) end if tree.ty ~= 2 then error("Expected tree, got " .. tree[tree.ty]) end return build_tree(objects, tree.data) end return { reader = reader, unpack = unpack, build_tree = build_tree, build_commit = build_commit, type = get_type, } end preload["network"] = function(...) local function pkt_line(msg) return ("%04x%s\n"):format(5 + #msg, msg) end local function pkt_linef(fmt, ...) return pkt_line(fmt:format(...)) end local flush_line = "0000" local function read_pkt_line(handle) local data = handle.read(4) if data == nil or data == "" then return nil end local len = tonumber(data, 16) if len == nil then error(("read_pkt_line: cannot convert %q to a number"):format(data)) elseif len == 0 then return false, data else return handle.read(len - 4), data end end local function fetch(url, lines, content_type) if type(lines) == "table" then lines = table.concat(lines) end local ok, err = http.request(url, lines, { ['User-Agent'] = 'CCGit/1.0', ['Content-Type'] = content_type, }, true) if ok then while true do local event, event_url, param1, param2 = os.pullEvent() if event == "http_success" and event_url == url then return true, param1 elseif event == "http_failure" and event_url == url then printError("Cannot fetch " .. url .. ": " .. param1) return false, param2 end end else printError("Cannot fetch " .. url .. ": " .. err) return false, nil end end local function force_fetch(...) local ok, handle, err_handle = fetch(...) if not ok then if err_handle then print(err_handle.getStatusCode()) print(textutils.serialize(err_handle.getResponseHeaders())) print(err_handle.readAll()) end error("Cannot fetch", 0) end return handle end local function receive(handle) local out = {} while true do local line = read_pkt_line(handle) if line == nil then break end out[#out + 1] = line end handle.close() return out end return { read_pkt_line = read_pkt_line, force_fetch = force_fetch, receive = receive, pkt_linef = pkt_linef, flush_line = flush_line, } end preload["deflate"] = function(...) --[[ (c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. (end license) --]] local assert, error, ipairs, pairs, tostring, type, setmetatable, io, math = assert, error, ipairs, pairs, tostring, type, setmetatable, io, math local table_sort, math_max, string_char = table.sort, math.max, string.char local band, lshift, rshift = bit32.band, bit32.lshift, bit32.rshift local function make_outstate(outbs) local outstate = {} outstate.outbs = outbs outstate.len = 0 outstate.window = {} outstate.window_pos = 1 return outstate end local function output(outstate, byte) local window_pos = outstate.window_pos outstate.outbs(byte) outstate.len = outstate.len + 1 outstate.window[window_pos] = byte outstate.window_pos = window_pos % 32768 + 1 -- 32K end local function noeof(val) return assert(val, 'unexpected end of file') end local function memoize(f) return setmetatable({}, { __index = function(self, k) local v = f(k) self[k] = v return v end }) end -- small optimization (lookup table for powers of 2) local pow2 = memoize(function(n) return 2^n end) local function bitstream_from_bytestream(bys) local buf_byte = 0 local buf_nbit = 0 local o = { type = "bitstream" } function o:nbits_left_in_byte() return buf_nbit end function o:read(nbits) nbits = nbits or 1 while buf_nbit < nbits do local byte = bys() if not byte then return end -- note: more calls also return nil buf_byte = buf_byte + lshift(byte, buf_nbit) buf_nbit = buf_nbit + 8 end local bits if nbits == 0 then bits = 0 elseif nbits == 32 then bits = buf_byte buf_byte = 0 else bits = band(buf_byte, rshift(0xffffffff, 32 - nbits)) buf_byte = rshift(buf_byte, nbits) end buf_nbit = buf_nbit - nbits return bits end return o end local function get_bitstream(o) if type(o) == "table" and o.type == "bitstream" then return o elseif io.type(o) == 'file' then return bitstream_from_bytestream(function() local sb = o:read(1) if sb then return sb:byte() end end) elseif type(o) == "function" then return bitstream_from_bytestream(o) else error 'unrecognized type' end end local function get_obytestream(o) local bs if io.type(o) == 'file' then bs = function(sbyte) o:write(string_char(sbyte)) end elseif type(o) == 'function' then bs = o else error('unrecognized type: ' .. tostring(o)) end return bs end local function HuffmanTable(init, is_full) local t = {} if is_full then for val,nbits in pairs(init) do if nbits ~= 0 then t[#t+1] = {val=val, nbits=nbits} end end else for i=1,#init-2,2 do local firstval, nbits, nextval = init[i], init[i+1], init[i+2] if nbits ~= 0 then for val=firstval,nextval-1 do t[#t+1] = {val=val, nbits=nbits} end end end end table_sort(t, function(a,b) return a.nbits == b.nbits and a.val < b.val or a.nbits < b.nbits end) -- assign codes local code = 1 -- leading 1 marker local nbits = 0 for _, s in ipairs(t) do if s.nbits ~= nbits then code = code * pow2[s.nbits - nbits] nbits = s.nbits end s.code = code code = code + 1 end local minbits = math.huge local look = {} for _, s in ipairs(t) do minbits = math.min(minbits, s.nbits) look[s.code] = s.val end local msb = function(bits, nbits) local res = 0 for _ = 1, nbits do res = lshift(res, 1) + band(bits, 1) bits = rshift(bits, 1) end return res end local tfirstcode = memoize( function(bits) return pow2[minbits] + msb(bits, minbits) end) function t:read(bs) local code = 1 -- leading 1 marker local nbits = 0 while 1 do if nbits == 0 then -- small optimization (optional) code = tfirstcode[noeof(bs:read(minbits))] nbits = nbits + minbits else local b = noeof(bs:read()) nbits = nbits + 1 code = code * 2 + b -- MSB first end local val = look[code] if val then return val end end end return t end local function parse_zlib_header(bs) local cm = bs:read(4) -- Compression Method local cinfo = bs:read(4) -- Compression info local fcheck = bs:read(5) -- FLaGs: FCHECK (check bits for CMF and FLG) local fdict = bs:read(1) -- FLaGs: FDICT (present dictionary) local flevel = bs:read(2) -- FLaGs: FLEVEL (compression level) local cmf = cinfo * 16 + cm -- CMF (Compresion Method and flags) local flg = fcheck + fdict * 32 + flevel * 64 -- FLaGs if cm ~= 8 then -- not "deflate" error("unrecognized zlib compression method: " .. cm) end if cinfo > 7 then error("invalid zlib window size: cinfo=" .. cinfo) end local window_size = 2^(cinfo + 8) if (cmf*256 + flg) % 31 ~= 0 then error("invalid zlib header (bad fcheck sum)") end if fdict == 1 then error("FIX:TODO - FDICT not currently implemented") local dictid_ = bs:read(32) end return window_size end local function parse_huffmantables(bs) local hlit = bs:read(5) -- # of literal/length codes - 257 local hdist = bs:read(5) -- # of distance codes - 1 local hclen = noeof(bs:read(4)) -- # of code length codes - 4 local ncodelen_codes = hclen + 4 local codelen_init = {} local codelen_vals = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15} for i=1,ncodelen_codes do local nbits = bs:read(3) local val = codelen_vals[i] codelen_init[val] = nbits end local codelentable = HuffmanTable(codelen_init, true) local function decode(ncodes) local init = {} local nbits local val = 0 while val < ncodes do local codelen = codelentable:read(bs) --FIX:check nil? local nrepeat if codelen <= 15 then nrepeat = 1 nbits = codelen elseif codelen == 16 then nrepeat = 3 + noeof(bs:read(2)) -- nbits unchanged elseif codelen == 17 then nrepeat = 3 + noeof(bs:read(3)) nbits = 0 elseif codelen == 18 then nrepeat = 11 + noeof(bs:read(7)) nbits = 0 else error 'ASSERT' end for _ = 1, nrepeat do init[val] = nbits val = val + 1 end end local huffmantable = HuffmanTable(init, true) return huffmantable end local nlit_codes = hlit + 257 local ndist_codes = hdist + 1 local littable = decode(nlit_codes) local disttable = decode(ndist_codes) return littable, disttable end local tdecode_len_base local tdecode_len_nextrabits local tdecode_dist_base local tdecode_dist_nextrabits local function parse_compressed_item(bs, outstate, littable, disttable) local val = littable:read(bs) if val < 256 then -- literal output(outstate, val) elseif val == 256 then -- end of block return true else if not tdecode_len_base then local t = {[257]=3} local skip = 1 for i=258,285,4 do for j=i,i+3 do t[j] = t[j-1] + skip end if i ~= 258 then skip = skip * 2 end end t[285] = 258 tdecode_len_base = t end if not tdecode_len_nextrabits then local t = {} for i=257,285 do local j = math_max(i - 261, 0) t[i] = rshift(j, 2) end t[285] = 0 tdecode_len_nextrabits = t end local len_base = tdecode_len_base[val] local nextrabits = tdecode_len_nextrabits[val] local extrabits = bs:read(nextrabits) local len = len_base + extrabits if not tdecode_dist_base then local t = {[0]=1} local skip = 1 for i=1,29,2 do for j=i,i+1 do t[j] = t[j-1] + skip end if i ~= 1 then skip = skip * 2 end end tdecode_dist_base = t end if not tdecode_dist_nextrabits then local t = {} for i=0,29 do local j = math_max(i - 2, 0) t[i] = rshift(j, 1) end tdecode_dist_nextrabits = t end local dist_val = disttable:read(bs) local dist_base = tdecode_dist_base[dist_val] local dist_nextrabits = tdecode_dist_nextrabits[dist_val] local dist_extrabits = bs:read(dist_nextrabits) local dist = dist_base + dist_extrabits for _ = 1,len do local pos = (outstate.window_pos - 1 - dist) % 32768 + 1 -- 32K output(outstate, assert(outstate.window[pos], 'invalid distance')) end end return false end local function parse_block(bs, outstate) local bfinal = bs:read(1) local btype = bs:read(2) local BTYPE_NO_COMPRESSION = 0 local BTYPE_FIXED_HUFFMAN = 1 local BTYPE_DYNAMIC_HUFFMAN = 2 local _BTYPE_RESERVED = 3 if btype == BTYPE_NO_COMPRESSION then bs:read(bs:nbits_left_in_byte()) local len = bs:read(16) local _nlen = noeof(bs:read(16)) for i=1,len do local by = noeof(bs:read(8)) output(outstate, by) end elseif btype == BTYPE_FIXED_HUFFMAN or btype == BTYPE_DYNAMIC_HUFFMAN then local littable, disttable if btype == BTYPE_DYNAMIC_HUFFMAN then littable, disttable = parse_huffmantables(bs) else littable = HuffmanTable {0,8, 144,9, 256,7, 280,8, 288,nil} disttable = HuffmanTable {0,5, 32,nil} end repeat local is_done = parse_compressed_item( bs, outstate, littable, disttable) until is_done else error('unrecognized compression type '..btype) end return bfinal ~= 0 end local function inflate(t) local bs = get_bitstream(t.input) local outbs = get_obytestream(t.output) local outstate = make_outstate(outbs) repeat local is_final = parse_block(bs, outstate) until is_final end local function adler32(byte, crc) local s1 = crc % 65536 local s2 = (crc - s1) / 65536 s1 = (s1 + byte) % 65521 s2 = (s2 + s1) % 65521 return s2*65536 + s1 end -- 65521 is the largest prime smaller than 2^16 local function inflate_zlib(t) local bs = get_bitstream(t.input) local outbs = get_obytestream(t.output) local disable_crc = t.disable_crc if disable_crc == nil then disable_crc = false end local _window_size = parse_zlib_header(bs) local data_adler32 = 1 inflate { input=bs, output = disable_crc and outbs or function(byte) data_adler32 = adler32(byte, data_adler32) outbs(byte) end, len = t.len, } bs:read(bs:nbits_left_in_byte()) local b3 = bs:read(8) local b2 = bs:read(8) local b1 = bs:read(8) local b0 = bs:read(8) local expected_adler32 = ((b3*256 + b2)*256 + b1)*256 + b0 if not disable_crc then if data_adler32 ~= expected_adler32 then error('invalid compressed data--crc error') end end end return { inflate = inflate, inflate_zlib = inflate_zlib, } end preload["clone"] = function(...) --- Git clone in Lua, from the bottom up -- -- http://stefan.saasen.me/articles/git-clone-in-haskell-from-the-bottom-up/#the_clone_process -- https://github.com/creationix/lua-git do -- metis loader local modules = { ["metis.argparse"] = "src/metis/argparse.lua", ["metis.crypto.sha1"] = "src/metis/crypto/sha1.lua", ["metis.timer"] = "src/metis/timer.lua", } package.loaders[#package.loaders + 1] = function(name) local path = modules[name] if not path then return nil, "not a metis module" end local local_path = "/.cache/metis/ae11085f261e5b506654162c80d21954c0d54e5e/" .. path if not fs.exists(local_path) then local url = "https://raw.githubusercontent.com/SquidDev-CC/metis/ae11085f261e5b506654162c80d21954c0d54e5e/" .. path local request, err = http.get(url) if not request then return nil, "Cannot download " .. url .. ": " .. err end local out = fs.open(local_path, "w") out.write(request.readAll()) out.close() request.close() end local fn, err = loadfile(local_path, nil, _ENV) if fn then return fn, local_path else return nil, err end end end local network = require "network" local objects = require "objects" local url, name = ... if not url or url == "-h" or url == "--help" then error("clone.lua URL [name]", 0) end if url:sub(-1) == "/" then url = url:sub(1, -2) end name = name or fs.getName(url):gsub("%.git$", "") local destination = shell.resolve(name) if fs.exists(destination) then error(("%q already exists"):format(name), 0) end local function report(msg) local last = "" for line in msg:gmatch("[^\n]+") do last = line end if replaceLine then term.setCursorPos(1, select(2, term.getCursorPos())) term.clearLine() term.write(last) else print(last) end end local head do -- Request a list of all refs report("Cloning from " .. url) local handle = network.force_fetch(url .. "/info/refs?service=git-upload-pack") local res = network.receive(handle) local sha_ptrn = ("%x"):rep(40) local caps = {} local refs = {} for i = 1, #res do local line = res[i] if line ~= false and line:sub(1, 1) ~= "#" then local sha, name = line:match("(" .. sha_ptrn .. ") ([^%z\n]+)") if sha and name then refs[name] = sha local capData = line:match("%z([^\n]+)\n") if capData then for cap in (capData .. " "):gmatch("%S+") do local eq = cap:find("=") if eq then caps[cap:sub(1, eq - 1)] = cap:sub(eq + 1) else caps[cap] = true end end end else printError("Unexpected line: " .. line) end end end head = refs['HEAD'] or refs['refs/heads/master'] or error("Cannot find master", 0) if not caps['shallow'] then error("Server does not support shallow fetching", 0) end -- TODO: Handle both. We don't even need the side-band really? if not caps['side-band-64k'] then error("Server does not support side band", 0) end end do -- Now actually perform the clone local handle = network.force_fetch(url .. "/git-upload-pack", { network.pkt_linef("want %s side-band-64k shallow", head), network.pkt_linef("deepen 1"), network.flush_line, network.pkt_linef("done"), }, "application/x-git-upload-pack-request") local pack, head = {}, nil while true do local line = network.read_pkt_line(handle) if line == nil then break end if line == false or line == "NAK\n" then -- Skip elseif line:byte(1) == 1 then table.insert(pack, line:sub(2)) elseif line:byte(1) == 2 or line:byte(1) == 3 then report(line:sub(2):gsub("\r", "\n")) elseif line:find("^shallow ") then head = line:sub(#("shallow ") + 1) else printError("Unknown line: " .. tostring(line)) end end handle.close() local stream = objects.reader(table.concat(pack)) local objs = objects.unpack(stream, function(x, n) report(("Extracting %d/%d (%.2f%%)"):format(x, n, x/n*100)) end) stream.close() if not head then error("Cannot find HEAD commit", 0) end for k, v in pairs(objects.build_commit(objs, head)) do local out = fs.open(fs.combine(destination, fs.combine(k, "")), "wb") out.write(v) out.close() end end report(("Cloned to %q"):format(name)) print() end return preload["clone"](...)