cgit — Lua Integration

Overview

cgit supports Lua as an in-process scripting language for content filters. Lua filters avoid the fork/exec overhead of shell-based filters and have direct access to cgit's HTML output functions. Lua support is optional and auto-detected at compile time.

Source files: filter.c (Lua filter implementation), cgit.mk (Lua detection).

Compile-Time Detection

Lua support is detected by cgit.mk using pkg-config:

ifndef NO_LUA
LUAPKGS := luajit lua lua5.2 lua5.1
LUAPKG := $(shell for p in $(LUAPKGS); do \
    $(PKG_CONFIG) --exists $$p 2>/dev/null && echo $$p && break; done)
ifneq ($(LUAPKG),)
    CGIT_CFLAGS += -DHAVE_LUA $(shell $(PKG_CONFIG) --cflags $(LUAPKG))
    CGIT_LIBS += $(shell $(PKG_CONFIG) --libs $(LUAPKG))
endif
endif

Detection order: luajitlualua5.2lua5.1.

To disable Lua even when available:

make NO_LUA=1

The HAVE_LUA preprocessor define gates all Lua-related code:

#ifdef HAVE_LUA
/* Lua filter implementation */
#else
/* stub: cgit_new_filter() returns NULL for lua: prefix */
#endif

Lua Filter Structure

struct cgit_lua_filter {
    struct cgit_filter base;   /* common filter fields */
    char *script_file;         /* path to Lua script */
    lua_State *lua_state;      /* Lua interpreter state */
};

The lua_State is lazily initialized on first use and reused for subsequent invocations of the same filter.

C API Exposed to Lua

cgit registers these C functions in the Lua environment:

html(str)

Writes raw HTML to stdout (no escaping):

static int lua_html(lua_State *L)
{
    const char *str = luaL_checkstring(L, 1);
    html(str);
    return 0;
}

html_txt(str)

Writes HTML-escaped text:

static int lua_html_txt(lua_State *L)
{
    const char *str = luaL_checkstring(L, 1);
    html_txt(str);
    return 0;
}

html_attr(str)

Writes attribute-escaped text:

static int lua_html_attr(lua_State *L)
{
    const char *str = luaL_checkstring(L, 1);
    html_attr(str);
    return 0;
}

html_url_path(str)

Writes a URL-encoded path:

static int lua_html_url_path(lua_State *L)
{
    const char *str = luaL_checkstring(L, 1);
    html_url_path(str);
    return 0;
}

html_url_arg(str)

Writes a URL-encoded query argument:

static int lua_html_url_arg(lua_State *L)
{
    const char *str = luaL_checkstring(L, 1);
    html_url_arg(str);
    return 0;
}

html_include(filename)

Includes a file's contents in the output:

static int lua_html_include(lua_State *L)
{
    const char *filename = luaL_checkstring(L, 1);
    html_include(filename);
    return 0;
}

Lua Filter Lifecycle

Initialization

On first open(), the Lua state is created and the script is loaded:

static int open_lua_filter(struct cgit_filter *base, ...)
{
    struct cgit_lua_filter *f = (struct cgit_lua_filter *)base;

    if (!f->lua_state) {
        /* Create new Lua state */
        f->lua_state = luaL_newstate();
        luaL_openlibs(f->lua_state);

        /* Register C functions */
        lua_pushcfunction(f->lua_state, lua_html);
        lua_setglobal(f->lua_state, "html");
        lua_pushcfunction(f->lua_state, lua_html_txt);
        lua_setglobal(f->lua_state, "html_txt");
        lua_pushcfunction(f->lua_state, lua_html_attr);
        lua_setglobal(f->lua_state, "html_attr");
        lua_pushcfunction(f->lua_state, lua_html_url_path);
        lua_setglobal(f->lua_state, "html_url_path");
        lua_pushcfunction(f->lua_state, lua_html_url_arg);
        lua_setglobal(f->lua_state, "html_url_arg");
        lua_pushcfunction(f->lua_state, lua_html_include);
        lua_setglobal(f->lua_state, "include");

        /* Load and execute the script file */
        if (luaL_dofile(f->lua_state, f->script_file))
            die("lua error: %s",
                lua_tostring(f->lua_state, -1));
    }

    /* Redirect stdout writes to lua write() function */

    /* Call filter_open() with filter-specific arguments */
    lua_getglobal(f->lua_state, "filter_open");
    /* push arguments from va_list */
    lua_call(f->lua_state, nargs, 0);

    return 0;
}

Data Flow

While the filter is open, data written to stdout is intercepted via a custom write() function:

/* The fprintf callback for Lua filters */
static void lua_fprintf(struct cgit_filter *base, FILE *f,
                        const char *fmt, ...)
{
    struct cgit_lua_filter *lf = (struct cgit_lua_filter *)base;
    /* format the string */
    /* call the Lua write() function with the formatted text */
    lua_getglobal(lf->lua_state, "write");
    lua_pushstring(lf->lua_state, buf);
    lua_call(lf->lua_state, 1, 0);
}

Close

static int close_lua_filter(struct cgit_filter *base)
{
    struct cgit_lua_filter *f = (struct cgit_lua_filter *)base;

    /* Call filter_close() */
    lua_getglobal(f->lua_state, "filter_close");
    lua_call(f->lua_state, 0, 1);

    /* Get return code */
    int rc = lua_tointeger(f->lua_state, -1);
    lua_pop(f->lua_state, 1);

    return rc;
}

Cleanup

static void cleanup_lua_filter(struct cgit_filter *base)
{
    struct cgit_lua_filter *f = (struct cgit_lua_filter *)base;
    if (f->lua_state)
        lua_close(f->lua_state);
}

Lua Script Interface

Required Functions

A Lua filter script must define these functions:

function filter_open(...)
    -- Called when the filter opens
    -- Arguments are filter-type specific
end

function write(str)
    -- Called with content chunks to process
    -- Transform and output using html() functions
end

function filter_close()
    -- Called when filtering is complete
    return 0  -- return exit code
end

Available Global Functions

Function Description
html(str) Output raw HTML
html_txt(str) Output HTML-escaped text
html_attr(str) Output attribute-escaped text
html_url_path(str) Output URL-path-encoded text
html_url_arg(str) Output URL-argument-encoded text
include(filename) Include file contents in output

All standard Lua libraries are available (string, table, math, io, os, etc.).

Example Filters

Source Highlighting Filter

-- syntax-highlighting.lua
local filename = ""
local buffer = {}

function filter_open(fn)
    filename = fn
    buffer = {}
end

function write(str)
    table.insert(buffer, str)
end

function filter_close()
    local content = table.concat(buffer)
    local ext = filename:match("%.(%w+)$") or ""

    -- Simple keyword highlighting
    local keywords = {
        ["function"] = true, ["local"] = true,
        ["if"] = true, ["then"] = true,
        ["end"] = true, ["return"] = true,
        ["for"] = true, ["while"] = true,
        ["do"] = true, ["else"] = true,
    }

    html("<pre><code>")
    for line in content:gmatch("([^\n]*)\n?") do
        html_txt(line)
        html("\n")
    end
    html("</code></pre>")

    return 0
end

Email Obfuscation Filter

-- email-obfuscate.lua
function filter_open(email, page)
    -- email = the email address
    -- page = current page name
end

function write(str)
    -- Replace @ with [at] for display
    local obfuscated = str:gsub("@", " [at] ")
    html_txt(obfuscated)
end

function filter_close()
    return 0
end

About/README Filter

-- about-markdown.lua
local buffer = {}

function filter_open(filename)
    buffer = {}
end

function write(str)
    table.insert(buffer, str)
end

function filter_close()
    local content = table.concat(buffer)
    -- Process markdown (using a Lua markdown library)
    -- or shell out to a converter
    local handle = io.popen("cmark", "w")
    handle:write(content)
    local result = handle:read("*a")
    handle:close()
    html(result)
    return 0
end

Auth Filter (Lua)

-- auth.lua
-- The auth filter receives 12 arguments
function filter_open(cookie, method, query, referer, path,
                     host, https, repo, page, accept, phase)
    if phase == "cookie" then
        -- Validate session cookie
        if valid_session(cookie) then
            return 0  -- authenticated
        end
        return 1  -- not authenticated
    elseif phase == "post" then
        -- Handle login form submission
    elseif phase == "authorize" then
        -- Check repository access
    end
end

function write(str)
    html(str)
end

function filter_close()
    return 0
end

Performance

Lua filters offer significant performance advantages over exec filters:

Aspect Exec Filter Lua Filter
Startup fork() + exec() per request One-time Lua state creation
Process New process per invocation In-process
Memory Separate address space Shared memory
Latency ~1-5ms fork overhead ~0.01ms function call
Libraries Any language Lua libraries only

Limitations

  • Lua scripts run in the same process as cgit — a crash in the script crashes cgit
  • Standard Lua I/O functions (print, io.write) bypass cgit's output pipeline — use html() and friends instead
  • The Lua state persists between invocations within the same CGI process, but CGI processes are typically short-lived
  • Error handling is via die() — a Lua error terminates the CGI process

Configuration

# Use Lua filter for source highlighting
source-filter=lua:/usr/share/cgit/filters/syntax-highlight.lua

# Use Lua filter for about pages
about-filter=lua:/usr/share/cgit/filters/about-markdown.lua

# Use Lua filter for authentication
auth-filter=lua:/usr/share/cgit/filters/simple-hierarchical-auth.lua

# Use Lua filter for email display
email-filter=lua:/usr/share/cgit/filters/email-libravatar.lua

Was this handbook page helpful?

This page is part of the Project Tick Handbook, which is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license. View full license details.
Last updated: April 18, 2026