-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. -- Manual end-to-end recipe (needs a running server): -- ./lilush examples/git_serve.lua --dir /srv/git --port 8090 & -- git clone http://127.0.0.1:8090/repo /tmp/clone-test -- git -C /tmp/clone-test fsck local testimony = require("testimony") local std = require("std") local http_git = require("http.git") local crypto = require("crypto") local testify = testimony.new("== http.git ==") local encode_pkt = http_git.encode_pkt local decode_pkts = http_git.decode_pkts -- ========================================================================= -- pkt-line -- ========================================================================= testify:that("encode_pkt: length prefix is hex of payload + 4", function() testimony.assert_equal("0009done\n", encode_pkt("done\n")) testimony.assert_equal("0004", encode_pkt("")) end) testify:that("decode_pkts: round-trips lines and flush", function() local body = encode_pkt("want abc\n") .. "0000" .. encode_pkt("done\n") local pkts = decode_pkts(body) testimony.assert_equal(3, #pkts) testimony.assert_equal("line", pkts[1].kind) testimony.assert_equal("want abc\n", pkts[1].data) testimony.assert_equal("flush", pkts[2].kind) testimony.assert_equal("done\n", pkts[3].data) end) testify:that("decode_pkts: rejects malformed length", function() local pkts, err = decode_pkts("zzzz") testimony.assert_nil(pkts) testimony.assert_match("malformed", err) end) testify:that("decode_pkts: rejects truncated pkt", function() local pkts, err = decode_pkts("00ffshort") testimony.assert_nil(pkts) testimony.assert_match("invalid pkt%-line length", err) end) -- ========================================================================= -- request parsing -- ========================================================================= local SHA_A = string.rep("a", 40) local SHA_B = string.rep("b", 40) testify:that("parse_upload_pack_request: wants, caps, haves, done", function() local body = encode_pkt("want " .. SHA_A .. " side-band-64k agent=git/2.40\n") .. encode_pkt("want " .. SHA_B .. "\n") .. "0000" .. encode_pkt("have " .. SHA_B .. "\n") .. encode_pkt("done\n") local req = http_git.parse_upload_pack_request(body) testimony.assert_equal(2, #req.wants) testimony.assert_equal(SHA_A, req.wants[1]) testimony.assert_true(req.side_band_64k) testimony.assert_false(req.side_band) testimony.assert_equal(1, req.haves) testimony.assert_true(req.done) end) testify:that("parse_upload_pack_request: rejects shallow/filter", function() local body = encode_pkt("want " .. SHA_A .. "\n") .. encode_pkt("deepen 1\n") .. "0000" local req, err = http_git.parse_upload_pack_request(body) testimony.assert_nil(req) testimony.assert_match("not supported", err) end) testify:that("parse_upload_pack_request: rejects empty request", function() local req, err = http_git.parse_upload_pack_request("0000") testimony.assert_nil(req) testimony.assert_match("no want lines", err) end) -- ========================================================================= -- Fixture-based tests (need the real git binary; skipped when absent) -- ========================================================================= -- os.execute returns a boolean with LUA52COMPAT, a number otherwise local sh = function(cmd) local r = os.execute(cmd .. " >/dev/null 2>&1") return r == true or r == 0 end local has_git = sh("git --version") local sh_out = function(cmd) local f = io.popen(cmd) if not f then return nil end local out = f:read("*a") f:close() return out and out:match("^%s*(.-)%s*$") end local make_fixture = function() local dir = "/tmp/" .. std.nanoid() local g = "git -C " .. dir .. " -c user.name=test -c user.email=test@test" if not sh("git init -q " .. dir) then return nil end std.fs.write_file(dir .. "/readme.txt", "hello lilush git serving\n") std.fs.mkdir(dir .. "/sub") std.fs.write_file(dir .. "/sub/data.bin", string.rep("\0\1\2\3", 1024)) sh(g .. " add -A") sh(g .. " commit -q -m first") std.fs.write_file(dir .. "/readme.txt", "hello again\n") sh(g .. " add -A") sh(g .. " commit -q -m second") sh(g .. " tag -a v1 -m release-one") local head = sh_out("git -C " .. dir .. " rev-parse HEAD") local head_ref = sh_out("git -C " .. dir .. " symbolic-ref HEAD") if not head or #head ~= 40 then return nil end return { dir = dir, git_dir = dir .. "/.git", head = head, head_ref = head_ref } end local fixture = has_git and make_fixture() or nil if fixture then testify:that("info_refs: valid advertisement", function() local advert = http_git.info_refs(fixture.git_dir) local pkts = decode_pkts(advert) testimony.assert_equal("# service=git-upload-pack\n", pkts[1].data) testimony.assert_equal("flush", pkts[2].kind) -- First ref line is HEAD with NUL-separated capabilities local head_line = pkts[3].data testimony.assert_match("^" .. fixture.head .. " HEAD\0", head_line) testimony.assert_match("side%-band%-64k", head_line) testimony.assert_match("symref=HEAD:" .. fixture.head_ref, head_line) testimony.assert_equal("flush", pkts[#pkts].kind) end) testify:that("upload_pack: negotiation round without done gets NAK", function() local body = encode_pkt("want " .. fixture.head .. "\n") .. "0000" .. encode_pkt("have " .. SHA_A .. "\n") testimony.assert_equal(encode_pkt("NAK\n"), http_git.upload_pack(fixture.git_dir, body)) end) testify:that("upload_pack: shallow request yields ERR pkt", function() local body = encode_pkt("want " .. fixture.head .. "\n") .. encode_pkt("deepen 1\n") .. "0000" local response = http_git.upload_pack(fixture.git_dir, body) local pkts = decode_pkts(response) testimony.assert_match("^ERR ", pkts[1].data) end) testify:that("upload_pack: side-band response carries a git-valid pack", function() local body = encode_pkt("want " .. fixture.head .. " side-band-64k\n") .. "0000" .. encode_pkt("done\n") local response = http_git.upload_pack(fixture.git_dir, body) local pkts = decode_pkts(response) testimony.assert_equal("NAK\n", pkts[1].data) testimony.assert_equal("flush", pkts[#pkts].kind) local parts = {} for i = 2, #pkts - 1 do testimony.assert_equal("\1", pkts[i].data:sub(1, 1)) parts[#parts + 1] = pkts[i].data:sub(2) end local pack = table.concat(parts) local pack_path = fixture.dir .. "/served.pack" std.fs.write_file(pack_path, pack) testimony.assert_true(sh("git index-pack --strict " .. pack_path)) end) testify:that("upload_pack: plain (no side-band) response is NAK + raw pack", function() local body = encode_pkt("want " .. fixture.head .. "\n") .. "0000" .. encode_pkt("done\n") local response = http_git.upload_pack(fixture.git_dir, body) testimony.assert_equal(encode_pkt("NAK\n"), response:sub(1, 8)) testimony.assert_equal("PACK", response:sub(9, 12)) testimony.assert_equal(crypto.sha1(response:sub(9, -21)), response:sub(-20)) end) testify:that("serve: handler routes advertisement, 404, and receive-pack refusal", function() local handle = http_git.serve({ ["/repo"] = fixture.dir }) testimony.assert_not_nil(handle) local content, status, headers = handle("GET", "/repo/info/refs", "service=git-upload-pack", {}, nil, {}) testimony.assert_equal(200, status) testimony.assert_equal("application/x-git-upload-pack-advertisement", headers["content-type"]) testimony.assert_equal("001e# service=git-upload-pack\n", content:sub(1, 30)) local _, dumb_status = handle("GET", "/repo/info/refs", "", {}, nil, {}) testimony.assert_equal(403, dumb_status) local _, missing_status = handle("GET", "/nosuch/info/refs", "service=git-upload-pack", {}, nil, {}) testimony.assert_equal(404, missing_status) local _, push_status = handle("POST", "/repo/git-receive-pack", "", {}, "", {}) testimony.assert_equal(403, push_status) local body = encode_pkt("want " .. fixture.head .. "\n") .. "0000" .. encode_pkt("done\n") local pack_content, pack_status, pack_headers = handle("POST", "/repo/git-upload-pack", "", {}, body, {}) testimony.assert_equal(200, pack_status) testimony.assert_equal("application/x-git-upload-pack-result", pack_headers["content-type"]) testimony.assert_equal("PACK", pack_content:sub(9, 12)) end) testify:that("serve: url with .git suffix resolves the same repo", function() local handle = http_git.serve({ ["/repo"] = fixture.dir }) local _, status = handle("GET", "/repo.git/info/refs", "service=git-upload-pack", {}, nil, {}) testimony.assert_equal(200, status) end) else print("-- git binary not found (or fixture failed), skipping fixture-based tests --") end if fixture then os.execute("rm -rf " .. fixture.dir) end testify:conclude()