diff options
Diffstat (limited to 'docs/handbook/cgit')
| -rw-r--r-- | docs/handbook/cgit/api-reference.md | 468 | ||||
| -rw-r--r-- | docs/handbook/cgit/architecture.md | 422 | ||||
| -rw-r--r-- | docs/handbook/cgit/authentication.md | 288 | ||||
| -rw-r--r-- | docs/handbook/cgit/building.md | 272 | ||||
| -rw-r--r-- | docs/handbook/cgit/caching-system.md | 287 | ||||
| -rw-r--r-- | docs/handbook/cgit/code-style.md | 356 | ||||
| -rw-r--r-- | docs/handbook/cgit/configuration.md | 351 | ||||
| -rw-r--r-- | docs/handbook/cgit/css-theming.md | 522 | ||||
| -rw-r--r-- | docs/handbook/cgit/deployment.md | 369 | ||||
| -rw-r--r-- | docs/handbook/cgit/diff-engine.md | 352 | ||||
| -rw-r--r-- | docs/handbook/cgit/filter-system.md | 358 | ||||
| -rw-r--r-- | docs/handbook/cgit/html-rendering.md | 380 | ||||
| -rw-r--r-- | docs/handbook/cgit/lua-integration.md | 428 | ||||
| -rw-r--r-- | docs/handbook/cgit/overview.md | 262 | ||||
| -rw-r--r-- | docs/handbook/cgit/repository-discovery.md | 355 | ||||
| -rw-r--r-- | docs/handbook/cgit/snapshot-system.md | 246 | ||||
| -rw-r--r-- | docs/handbook/cgit/testing.md | 335 | ||||
| -rw-r--r-- | docs/handbook/cgit/ui-modules.md | 544 | ||||
| -rw-r--r-- | docs/handbook/cgit/url-routing.md | 331 |
19 files changed, 6926 insertions, 0 deletions
diff --git a/docs/handbook/cgit/api-reference.md b/docs/handbook/cgit/api-reference.md new file mode 100644 index 0000000000..0c38564e74 --- /dev/null +++ b/docs/handbook/cgit/api-reference.md @@ -0,0 +1,468 @@ +# cgit — API Reference + +## Overview + +This document catalogs all public function prototypes, types, and global +variables exported by cgit's header files. Functions are grouped by header +file and module. + +## `cgit.h` — Core Types and Functions + +### Core Structures + +```c +struct cgit_environment { + const char *cgit_config; /* CGIT_CONFIG env var */ + const char *http_host; /* HTTP_HOST */ + const char *https; /* HTTPS */ + const char *no_http; /* NO_HTTP */ + const char *http_cookie; /* HTTP_COOKIE */ + const char *request_method; /* REQUEST_METHOD */ + const char *query_string; /* QUERY_STRING */ + const char *http_referer; /* HTTP_REFERER */ + const char *path_info; /* PATH_INFO */ + const char *script_name; /* SCRIPT_NAME */ + const char *server_name; /* SERVER_NAME */ + const char *server_port; /* SERVER_PORT */ + const char *http_accept; /* HTTP_ACCEPT */ + int authenticated; /* authentication result */ +}; + +struct cgit_query { + char *raw; + char *repo; + char *page; + char *search; + char *grep; + char *head; + char *sha1; + char *sha2; + char *path; + char *name; + char *url; + char *mimetype; + char *etag; + int nohead; + int ofs; + int has_symref; + int has_sha1; + int has_dot; + int ignored; + char *sort; + int showmsg; + int ssdiff; + int show_all; + int context; + int follow; + int dt; + int log_hierarchical_threading; +}; + +struct cgit_page { + const char *mimetype; + const char *charset; + const char *filename; + const char *etag; + const char *title; + int status; + time_t modified; + time_t expires; + size_t size; +}; + +struct cgit_config { + char *root_title; + char *root_desc; + char *root_readme; + char *root_coc; + char *root_cla; + char *root_homepage; + char *root_homepage_title; + struct string_list root_links; + char *css; + struct string_list css_list; + char *js; + struct string_list js_list; + char *logo; + char *logo_link; + char *favicon; + char *header; + char *footer; + char *head_include; + char *module_link; + char *virtual_root; + char *script_name; + char *section; + char *cache_root; + char *robots; + char *clone_prefix; + char *clone_url; + char *readme; + char *agefile; + char *project_list; + char *strict_export; + char *mimetype_file; + /* ... filter pointers, integer flags, limits ... */ + int cache_size; + int cache_root_ttl; + int cache_repo_ttl; + int cache_dynamic_ttl; + int cache_static_ttl; + int cache_about_ttl; + int cache_snapshot_ttl; + int cache_scanrc_ttl; + int max_repo_count; + int max_commit_count; + int max_message_length; + int max_repodesc_length; + int max_blob_size; + int max_stats; + int max_atom_items; + int max_subtree_commits; + int summary_branches; + int summary_tags; + int summary_log; + int snapshots; + int enable_http_clone; + int enable_index_links; + int enable_index_owner; + int enable_blame; + int enable_commit_graph; + int enable_log_filecount; + int enable_log_linecount; + int enable_remote_branches; + int enable_subject_links; + int enable_html_serving; + int enable_subtree; + int enable_tree_linenumbers; + int enable_git_config; + int enable_filter_overrides; + int enable_follow_links; + int embedded; + int noheader; + int noplainemail; + int local_time; + int case_sensitive_sort; + int section_sort; + int section_from_path; + int side_by_side_diffs; + int remove_suffix; + int scan_hidden_path; + int branch_sort; + int commit_sort; + int renamelimit; +}; + +struct cgit_repo { + char *url; + char *name; + char *basename; + char *path; + char *desc; + char *owner; + char *homepage; + char *defbranch; + char *section; + char *clone_url; + char *logo; + char *logo_link; + char *readme; + char *module_link; + char *extra_head_content; + char *snapshot_prefix; + struct string_list badges; + struct cgit_filter *about_filter; + struct cgit_filter *commit_filter; + struct cgit_filter *source_filter; + struct cgit_filter *email_filter; + struct cgit_filter *owner_filter; + int snapshots; + int enable_blame; + int enable_commit_graph; + int enable_log_filecount; + int enable_log_linecount; + int enable_remote_branches; + int enable_subject_links; + int enable_html_serving; + int enable_subtree; + int max_stats; + int max_subtree_commits; + int branch_sort; + int commit_sort; + int hide; + int ignore; +}; + +struct cgit_context { + struct cgit_environment env; + struct cgit_query qry; + struct cgit_config cfg; + struct cgit_page page; + struct cgit_repo *repo; +}; +``` + +### Global Variables + +```c +extern struct cgit_context ctx; +extern struct cgit_repolist cgit_repolist; +extern const char *cgit_version; +``` + +### Repository Management + +```c +extern struct cgit_repo *cgit_add_repo(const char *url); +extern struct cgit_repo *cgit_get_repoinfo(const char *url); +``` + +### Parsing Functions + +```c +extern void cgit_parse_url(const char *url); +extern struct commitinfo *cgit_parse_commit(struct commit *commit); +extern struct taginfo *cgit_parse_tag(struct tag *tag); +extern void cgit_free_commitinfo(struct commitinfo *info); +extern void cgit_free_taginfo(struct taginfo *info); +``` + +### Diff Functions + +```c +typedef void (*filepair_fn)(struct diff_filepair *pair); +typedef void (*linediff_fn)(char *line, int len); + +extern void cgit_diff_tree(const struct object_id *old_oid, + const struct object_id *new_oid, + filepair_fn fn, const char *prefix, + int renamelimit); +extern void cgit_diff_commit(struct commit *commit, filepair_fn fn, + const char *prefix); +extern void cgit_diff_files(const struct object_id *old_oid, + const struct object_id *new_oid, + unsigned long *old_size, + unsigned long *new_size, + int *binary, int context, + int ignorews, linediff_fn fn); +``` + +### Snapshot Functions + +```c +extern int cgit_parse_snapshots_mask(const char *str); + +extern const struct cgit_snapshot_format cgit_snapshot_formats[]; +``` + +### Filter Functions + +```c +extern struct cgit_filter *cgit_new_filter(const char *cmd, filter_type type); +extern int cgit_open_filter(struct cgit_filter *filter, ...); +extern int cgit_close_filter(struct cgit_filter *filter); +``` + +### Utility Functions + +```c +extern const char *cgit_repobasename(const char *reponame); +extern char *cgit_default_repo_desc; +extern int cgit_ref_path_exists(const char *path, const char *ref, int file_only); +``` + +## `html.h` — HTML Output Functions + +```c +extern const char *fmt(const char *format, ...); +extern char *fmtalloc(const char *format, ...); + +extern void html_raw(const char *data, size_t size); +extern void html(const char *txt); +extern void htmlf(const char *format, ...); +extern void html_txt(const char *txt); +extern void html_ntxt(const char *txt, int len); +extern void html_attr(const char *txt); +extern void html_url_path(const char *txt); +extern void html_url_arg(const char *txt); +extern void html_hidden(const char *name, const char *value); +extern void html_option(const char *value, const char *text, + const char *selected_value); +extern void html_link_open(const char *url, const char *title, + const char *class); +extern void html_link_close(void); +extern void html_include(const char *filename); +extern void html_checkbox(const char *name, int value); +extern void html_txt_input(const char *name, const char *value, int size); +``` + +## `ui-shared.h` — Page Layout and Links + +### HTTP and Layout + +```c +extern void cgit_print_http_headers(void); +extern void cgit_print_docstart(void); +extern void cgit_print_docend(void); +extern void cgit_print_pageheader(void); +extern void cgit_print_layout_start(void); +extern void cgit_print_layout_end(void); +extern void cgit_print_error(const char *msg); +extern void cgit_print_error_page(int code, const char *msg, + const char *fmt, ...); +``` + +### URL Generation + +```c +extern const char *cgit_repourl(const char *reponame); +extern const char *cgit_fileurl(const char *reponame, const char *pagename, + const char *filename, const char *query); +extern const char *cgit_pageurl(const char *reponame, const char *pagename, + const char *query); +extern const char *cgit_currurl(void); +extern const char *cgit_rooturl(void); +``` + +### Link Functions + +```c +extern void cgit_summary_link(const char *name, const char *title, + const char *class, const char *head); +extern void cgit_tag_link(const char *name, const char *title, + const char *class, const char *tag); +extern void cgit_tree_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +extern void cgit_log_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path, + int ofs, const char *grep, const char *pattern, + int showmsg, int follow); +extern void cgit_commit_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +extern void cgit_patch_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +extern void cgit_refs_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +extern void cgit_diff_link(const char *name, const char *title, + const char *class, const char *head, + const char *new_rev, const char *old_rev, + const char *path, int toggle); +extern void cgit_stats_link(const char *name, const char *title, + const char *class, const char *head, + const char *path); +extern void cgit_plain_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +extern void cgit_blame_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +extern void cgit_object_link(struct object *obj); +extern void cgit_submodule_link(const char *name, const char *path, + const char *commit); +extern void cgit_print_snapshot_links(const char *repo, const char *head, + const char *hex, int snapshots); +extern void cgit_print_branches(int max); +extern void cgit_print_tags(int max); +``` + +### Diff Helpers + +```c +extern void cgit_print_diff_hunk_header(int oldofs, int oldcnt, + int newofs, int newcnt, + const char *func); +extern void cgit_print_diff_line_prefix(int type); +``` + +## `cmd.h` — Command Dispatch + +```c +struct cgit_cmd { + const char *name; + void (*fn)(struct cgit_context *ctx); + unsigned int want_hierarchical:1; + unsigned int want_repo:1; + unsigned int want_layout:1; + unsigned int want_vpath:1; + unsigned int is_clone:1; +}; + +extern struct cgit_cmd *cgit_get_cmd(const char *name); +``` + +## `cache.h` — Cache System + +```c +typedef void (*cache_fill_fn)(void *cbdata); + +extern int cache_process(int size, const char *path, const char *key, + int ttl, cache_fill_fn fn, void *cbdata); +extern int cache_ls(const char *path); +extern unsigned long hash_str(const char *str); +``` + +## `configfile.h` — Configuration File Parser + +```c +typedef void (*configfile_value_fn)(const char *name, const char *value); + +extern int parse_configfile(const char *filename, configfile_value_fn fn); +``` + +## `scan-tree.h` — Repository Scanner + +```c +typedef void (*repo_config_fn)(struct cgit_repo *repo, + const char *name, const char *value); + +extern void scan_projects(const char *path, const char *projectsfile, + repo_config_fn fn); +extern void scan_tree(const char *path, repo_config_fn fn); +``` + +## `filter.c` — Filter Types + +```c +#define ABOUT_FILTER 0 +#define COMMIT_FILTER 1 +#define SOURCE_FILTER 2 +#define EMAIL_FILTER 3 +#define AUTH_FILTER 4 +#define OWNER_FILTER 5 + +typedef int filter_type; +``` + +## UI Module Entry Points + +Each `ui-*.c` module exposes one or more public functions: + +| Module | Function | Description | +|--------|----------|-------------| +| `ui-atom.c` | `cgit_print_atom(char *tip, char *path, int max)` | Generate Atom feed | +| `ui-blame.c` | `cgit_print_blame(void)` | Render blame view | +| `ui-blob.c` | `cgit_print_blob(const char *hex, char *path, const char *head, int file_only)` | Display blob content | +| `ui-clone.c` | `cgit_clone_info(void)` | HTTP clone: `info/refs` | +| `ui-clone.c` | `cgit_clone_objects(void)` | HTTP clone: pack objects | +| `ui-clone.c` | `cgit_clone_head(void)` | HTTP clone: `HEAD` ref | +| `ui-commit.c` | `cgit_print_commit(const char *rev, const char *prefix)` | Display commit | +| `ui-diff.c` | `cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefix, int show_ctrls, int raw)` | Render diff | +| `ui-diff.c` | `cgit_print_diffstat(const struct object_id *old, const struct object_id *new, const char *prefix)` | Render diffstat | +| `ui-log.c` | `cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern, char *path, int pager, int commit_graph, int commit_sort)` | Display log | +| `ui-patch.c` | `cgit_print_patch(const char *new_rev, const char *old_rev, const char *prefix)` | Generate patch | +| `ui-plain.c` | `cgit_print_plain(void)` | Serve raw file content | +| `ui-refs.c` | `cgit_print_refs(void)` | Display branches and tags | +| `ui-repolist.c` | `cgit_print_repolist(void)` | Repository index page | +| `ui-snapshot.c` | `cgit_print_snapshot(const char *head, const char *hex, const char *prefix, const char *filename, int snapshots)` | Generate archive | +| `ui-stats.c` | `cgit_print_stats(void)` | Display statistics | +| `ui-summary.c` | `cgit_print_summary(void)` | Repository summary page | +| `ui-ssdiff.c` | `cgit_ssdiff_header_begin(void)` | Start ssdiff output | +| `ui-ssdiff.c` | `cgit_ssdiff_header_end(void)` | End ssdiff header | +| `ui-ssdiff.c` | `cgit_ssdiff_footer(void)` | End ssdiff output | +| `ui-tag.c` | `cgit_print_tag(const char *revname)` | Display tag | +| `ui-tree.c` | `cgit_print_tree(const char *rev, char *path)` | Display tree | diff --git a/docs/handbook/cgit/architecture.md b/docs/handbook/cgit/architecture.md new file mode 100644 index 0000000000..e35633a505 --- /dev/null +++ b/docs/handbook/cgit/architecture.md @@ -0,0 +1,422 @@ +# cgit — Architecture + +## High-Level Component Map + +``` +┌──────────────────────────────────────────────────────────────┐ +│ cgit.c │ +│ constructor_environment() [__attribute__((constructor))] │ +│ prepare_context() → config_cb() → querystring_cb() │ +│ authenticate_cookie() → process_request() → main() │ +├──────────────────────────────────────────────────────────────┤ +│ Command Dispatcher │ +│ cmd.c │ +│ cgit_get_cmd() → static cmds[] table (23 entries) │ +│ struct cgit_cmd { name, fn, want_repo, want_vpath, is_clone }│ +├──────────┬───────────┬───────────┬───────────────────────────┤ +│ UI Layer │ Caching │ Filters │ HTTP Clone │ +│ ui-*.c │ cache.c │ filter.c │ ui-clone.c │ +│ (17 mods)│ cache.h │ │ │ +├──────────┴───────────┴───────────┴───────────────────────────┤ +│ Core Utilities │ +│ shared.c — global vars, repo mgmt, diff wrappers │ +│ parsing.c — cgit_parse_commit(), cgit_parse_tag(), │ +│ cgit_parse_url() │ +│ html.c — entity escaping, URL encoding, form helpers │ +│ configfile.c — line-oriented name=value parser │ +│ scan-tree.c — filesystem repository discovery │ +├──────────────────────────────────────────────────────────────┤ +│ Vendored git library │ +│ git/ — full Git 2.46.0 source; linked via cgit.mk │ +│ Provides: object store, diff engine (xdiff), refs, revwalk, │ +│ archive, notes, commit graph, blame, packfile │ +└──────────────────────────────────────────────────────────────┘ +``` + +## Global State + +cgit uses a single global variable to carry all request state: + +```c +/* shared.c */ +struct cgit_repolist cgit_repolist; /* Array of all known repositories */ +struct cgit_context ctx; /* Current request context */ +``` + +### `struct cgit_context` + +```c +struct cgit_context { + struct cgit_environment env; /* CGI env vars (HTTP_HOST, QUERY_STRING, etc.) */ + struct cgit_query qry; /* Parsed URL/query parameters */ + struct cgit_config cfg; /* All global config directives */ + struct cgit_repo *repo; /* Pointer into cgit_repolist.repos[] or NULL */ + struct cgit_page page; /* HTTP response metadata (mimetype, status, etag) */ +}; +``` + +### `struct cgit_environment` + +Populated by `prepare_context()` via `getenv()`: + +```c +struct cgit_environment { + const char *cgit_config; /* $CGIT_CONFIG (default: /etc/cgitrc) */ + const char *http_host; /* $HTTP_HOST */ + const char *https; /* $HTTPS ("on" if TLS) */ + const char *no_http; /* $NO_HTTP (non-NULL → CLI mode) */ + const char *path_info; /* $PATH_INFO */ + const char *query_string; /* $QUERY_STRING */ + const char *request_method; /* $REQUEST_METHOD */ + const char *script_name; /* $SCRIPT_NAME */ + const char *server_name; /* $SERVER_NAME */ + const char *server_port; /* $SERVER_PORT */ + const char *http_cookie; /* $HTTP_COOKIE */ + const char *http_referer; /* $HTTP_REFERER */ + unsigned int content_length; /* $CONTENT_LENGTH */ + int authenticated; /* Set by auth filter (0 or 1) */ +}; +``` + +### `struct cgit_page` + +Controls HTTP response headers: + +```c +struct cgit_page { + time_t modified; /* Last-Modified header */ + time_t expires; /* Expires header */ + size_t size; /* Content-Length (0 = omit) */ + const char *mimetype; /* Content-Type (default: "text/html") */ + const char *charset; /* charset param (default: "UTF-8") */ + const char *filename; /* Content-Disposition filename */ + const char *etag; /* ETag header value */ + const char *title; /* HTML <title> */ + int status; /* HTTP status code (0 = 200) */ + const char *statusmsg; /* HTTP status message */ +}; +``` + +## Request Lifecycle — Detailed + +### Phase 1: Pre-main Initialization + +```c +__attribute__((constructor)) +static void constructor_environment() +{ + setenv("GIT_CONFIG_NOSYSTEM", "1", 1); + setenv("GIT_ATTR_NOSYSTEM", "1", 1); + unsetenv("HOME"); + unsetenv("XDG_CONFIG_HOME"); +} +``` + +This runs before `main()` on every invocation. It prevents Git from loading +`/etc/gitconfig`, `~/.gitconfig`, or any `$XDG_CONFIG_HOME/git/config`, ensuring +complete isolation from the host system's Git configuration. + +### Phase 2: Context Preparation + +`prepare_context()` zero-initializes `ctx` and sets every configuration field +to its default value. Key defaults: + +| Field | Default | +|-------|---------| +| `cfg.cache_size` | `0` (disabled) | +| `cfg.cache_root` | `CGIT_CACHE_ROOT` (`/var/cache/cgit`) | +| `cfg.cache_repo_ttl` | `5` minutes | +| `cfg.cache_root_ttl` | `5` minutes | +| `cfg.cache_static_ttl` | `-1` (never expires) | +| `cfg.max_repo_count` | `50` | +| `cfg.max_commit_count` | `50` | +| `cfg.max_msg_len` | `80` | +| `cfg.max_repodesc_len` | `80` | +| `cfg.enable_http_clone` | `1` | +| `cfg.enable_index_owner` | `1` | +| `cfg.enable_tree_linenumbers` | `1` | +| `cfg.summary_branches` | `10` | +| `cfg.summary_log` | `10` | +| `cfg.summary_tags` | `10` | +| `cfg.difftype` | `DIFF_UNIFIED` | +| `cfg.robots` | `"index, nofollow"` | +| `cfg.root_title` | `"Git repository browser"` | + +The function also reads all CGI environment variables and sets +`page.mimetype = "text/html"`, `page.charset = PAGE_ENCODING` (`"UTF-8"`). + +### Phase 3: Configuration Parsing + +```c +parse_configfile(ctx.env.cgit_config, config_cb); +``` + +`parse_configfile()` (in `configfile.c`) opens the file, reads lines of the +form `name=value`, skips comments (`#` and `;`), and calls the callback for each +directive. It supports recursive `include=` directives up to 8 levels deep. + +`config_cb()` (in `cgit.c`) is a ~200-line chain of `if/else if` blocks that +maps directive names to `ctx.cfg.*` fields. When `repo.url=` is encountered, +`cgit_add_repo()` allocates a new repository entry; subsequent `repo.*` +directives configure that entry via `repo_config()`. + +Special directive: `scan-path=` triggers immediate filesystem scanning via +`scan_tree()` or `scan_projects()`, or via a cached repolist file if +`cache-size > 0`. + +### Phase 4: Query String Parsing + +```c +http_parse_querystring(ctx.qry.raw, querystring_cb); +``` + +`querystring_cb()` maps short parameter names to `ctx.qry.*` fields: + +| Parameter | Field | Purpose | +|-----------|-------|---------| +| `r` | `qry.repo` | Repository URL | +| `p` | `qry.page` | Page name | +| `url` | `qry.url` | Combined repo/page/path | +| `h` | `qry.head` | Branch/ref | +| `id` | `qry.oid` | Object ID | +| `id2` | `qry.oid2` | Second object ID (for diffs) | +| `ofs` | `qry.ofs` | Pagination offset | +| `q` | `qry.search` | Search query | +| `qt` | `qry.grep` | Search type | +| `path` | `qry.path` | File path | +| `name` | `qry.name` | Snapshot filename | +| `dt` | `qry.difftype` | Diff type (0/1/2) | +| `context` | `qry.context` | Diff context lines | +| `ignorews` | `qry.ignorews` | Ignore whitespace | +| `follow` | `qry.follow` | Follow renames | +| `showmsg` | `qry.showmsg` | Show full messages | +| `s` | `qry.sort` | Sort order | +| `period` | `qry.period` | Stats period | + +The `url=` parameter receives special processing via `cgit_parse_url()` (in +`parsing.c`), which iteratively splits the URL at `/` characters, looking for +the longest prefix that matches a known repository URL. + +### Phase 5: Authentication + +`authenticate_cookie()` checks three cases: + +1. **No auth filter** → set `ctx.env.authenticated = 1` and return. +2. **POST to login page** → call `authenticate_post()`, which reads up to + `MAX_AUTHENTICATION_POST_BYTES` (4096) from stdin, pipes it to the auth + filter with function `"authenticate-post"`, and exits. +3. **Normal request** → invoke auth filter with function + `"authenticate-cookie"`. The filter's exit code becomes + `ctx.env.authenticated`. + +The auth filter receives 12 arguments: + +``` +function, cookie, method, query_string, http_referer, +path_info, http_host, https, repo, page, fullurl, loginurl +``` + +### Phase 6: Cache Envelope + +If `ctx.cfg.cache_size > 0`, the request is wrapped in `cache_process()`: + +```c +cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, + cache_key, ttl, fill_fn); +``` + +This constructs a filename from the FNV-1 hash of the cache key, attempts to +open an existing slot, verifies the key matches, checks expiry, and either +serves cached content or locks and fills a new slot. See the Caching System +document for full details. + +### Phase 7: Command Dispatch + +```c +cmd = cgit_get_cmd(); +``` + +`cgit_get_cmd()` (in `cmd.c`) performs a linear scan of the static `cmds[]` +table: + +```c +static struct cgit_cmd cmds[] = { + def_cmd(HEAD, 1, 0, 1), + def_cmd(atom, 1, 0, 0), + def_cmd(about, 0, 0, 0), + def_cmd(blame, 1, 1, 0), + def_cmd(blob, 1, 0, 0), + def_cmd(cla, 0, 0, 0), + def_cmd(commit, 1, 1, 0), + def_cmd(coc, 0, 0, 0), + def_cmd(diff, 1, 1, 0), + def_cmd(info, 1, 0, 1), + def_cmd(log, 1, 1, 0), + def_cmd(ls_cache, 0, 0, 0), + def_cmd(objects, 1, 0, 1), + def_cmd(patch, 1, 1, 0), + def_cmd(plain, 1, 0, 0), + def_cmd(rawdiff, 1, 1, 0), + def_cmd(refs, 1, 0, 0), + def_cmd(repolist, 0, 0, 0), + def_cmd(snapshot, 1, 0, 0), + def_cmd(stats, 1, 1, 0), + def_cmd(summary, 1, 0, 0), + def_cmd(tag, 1, 0, 0), + def_cmd(tree, 1, 1, 0), +}; +``` + +The `def_cmd` macro expands to `{#name, name##_fn, want_repo, want_vpath, is_clone}`. + +Default page if none specified: +- With a repository → `"summary"` +- Without a repository → `"repolist"` + +### Phase 8: Repository Preparation + +If `cmd->want_repo` is set: + +1. `prepare_repo_env()` calls `setenv("GIT_DIR", ctx.repo->path, 1)`, + `setup_git_directory_gently()`, and `load_display_notes()`. +2. `prepare_repo_cmd()` resolves the default branch (via `guess_defbranch()` + which checks `HEAD` → `refs/heads/*`), resolves the requested head to an OID, + sorts submodules, chooses the README file, and sets the page title. + +### Phase 9: Page Rendering + +The handler function (`cmd->fn()`) is called. Most handlers follow this +pattern: + +```c +cgit_print_layout_start(); /* HTTP headers + HTML doctype + header + tabs */ +/* ... page-specific content ... */ +cgit_print_layout_end(); /* footer + closing tags */ +``` + +`cgit_print_layout_start()` calls: +- `cgit_print_http_headers()` — Content-Type, Last-Modified, Expires, ETag +- `cgit_print_docstart()` — `<!DOCTYPE html>`, `<html>`, CSS/JS includes +- `cgit_print_pageheader()` — header table, navigation tabs, breadcrumbs + +## Module Dependency Graph + +``` +cgit.c ──→ cmd.c ──→ ui-*.c (all modules) + │ │ + │ └──→ cache.c + │ + ├──→ configfile.c + ├──→ scan-tree.c ──→ configfile.c + ├──→ ui-shared.c ──→ html.c + ├──→ ui-stats.c + ├──→ ui-blob.c + ├──→ ui-summary.c + └──→ filter.c + +ui-commit.c ──→ ui-diff.c ──→ ui-ssdiff.c +ui-summary.c ──→ ui-log.c, ui-refs.c, ui-blob.c, ui-plain.c +ui-log.c ──→ ui-shared.c +All ui-*.c ──→ html.c, ui-shared.c +``` + +## The `struct cgit_cmd` Pattern + +Each command in `cmd.c` is defined as a static function that wraps the +corresponding UI module: + +```c +static void log_fn(void) +{ + cgit_print_log(ctx.qry.oid, ctx.qry.ofs, ctx.cfg.max_commit_count, + ctx.qry.grep, ctx.qry.search, ctx.qry.path, 1, + ctx.repo->enable_commit_graph, + ctx.repo->commit_sort); +} +``` + +The thin wrapper pattern means all context is accessed via the global `ctx` +struct, and the wrapper simply extracts the relevant fields and passes them to +the module function. + +## Repository List Management + +The `cgit_repolist` global is a dynamically-growing array: + +```c +struct cgit_repolist { + int length; /* Allocated capacity */ + int count; /* Number of repos */ + struct cgit_repo *repos; /* Array */ +}; +``` + +`cgit_add_repo()` doubles the array capacity when needed (starting from 8). +Each new repo inherits defaults from `ctx.cfg.*` (snapshots, feature flags, +filters, etc.). + +`cgit_get_repoinfo()` performs a linear scan (O(n)) to find a repo by URL. +Ignored repos (`repo->ignore == 1`) are skipped. + +## Build System + +The build works in two stages: + +1. **Git build** — `make` in the top-level `cgit/` directory delegates to + `make -C git -f ../cgit.mk` which includes Git's own `Makefile`. + +2. **cgit link** — `cgit.mk` lists all cgit object files (`CGIT_OBJ_NAMES`), + compiles them with `CGIT_CFLAGS` (which embeds `CGIT_CONFIG`, + `CGIT_SCRIPT_NAME`, `CGIT_CACHE_ROOT` as string literals), and links them + against Git's `libgit.a`. + +Lua support is auto-detected via `pkg-config` (checking `luajit`, `lua`, +`lua5.2`, `lua5.1` in order). Define `NO_LUA=1` to build without Lua. +Linux systems get `HAVE_LINUX_SENDFILE` which enables the `sendfile()` syscall +in the cache layer. + +## Thread Safety + +cgit runs as a **single-process CGI** — one process per HTTP request. There is +no multi-threading. All global state (`ctx`, `cgit_repolist`, the static +`diffbuf` in `shared.c`, the static format buffers in `html.c`) is safe because +each process is fully isolated. + +The `fmt()` function in `html.c` uses a ring buffer of 8 static buffers +(`static char buf[8][1024]`) to allow up to 8 nested `fmt()` calls in a single +expression. The `bufidx` rotates via `bufidx = (bufidx + 1) & 7`. + +## Error Handling + +The codebase uses three assertion-style helpers from `shared.c`: + +```c +int chk_zero(int result, char *msg); /* die if result != 0 */ +int chk_positive(int result, char *msg); /* die if result <= 0 */ +int chk_non_negative(int result, char *msg); /* die if result < 0 */ +``` + +For user-facing errors, `cgit_print_error_page()` sets HTTP status, prints +headers, renders the page skeleton, and displays the error message. + +## Type System + +cgit uses three enums defined in `cgit.h`: + +```c +typedef enum { + DIFF_UNIFIED, DIFF_SSDIFF, DIFF_STATONLY +} diff_type; + +typedef enum { + ABOUT, COMMIT, SOURCE, EMAIL, AUTH, OWNER +} filter_type; +``` + +And three function pointer typedefs: + +```c +typedef void (*configfn)(const char *name, const char *value); +typedef void (*filepair_fn)(struct diff_filepair *pair); +typedef void (*linediff_fn)(char *line, int len); +``` diff --git a/docs/handbook/cgit/authentication.md b/docs/handbook/cgit/authentication.md new file mode 100644 index 0000000000..a4fe000a87 --- /dev/null +++ b/docs/handbook/cgit/authentication.md @@ -0,0 +1,288 @@ +# cgit — Authentication + +## Overview + +cgit supports cookie-based authentication through the `auth-filter` +mechanism. The authentication system intercepts requests before page +rendering and delegates all credential validation to an external filter +(exec or Lua script). + +Source file: `cgit.c` (authentication hooks), `filter.c` (filter execution). + +## Architecture + +Authentication is entirely filter-driven. cgit itself stores no credentials, +sessions, or user databases. The auth filter is responsible for: + +1. Rendering login forms +2. Validating credentials +3. Setting/reading session cookies +4. Determining authorization per-repository + +## Configuration + +```ini +auth-filter=lua:/path/to/auth.lua +# or +auth-filter=exec:/path/to/auth.sh +``` + +The auth filter type is `AUTH_FILTER` (constant `4`) and receives 12 +arguments. + +## Authentication Flow + +### Request Processing in `cgit.c` + +Authentication is checked in `process_request()` after URL parsing and +command dispatch: + +```c +/* In process_request() */ +if (ctx.cfg.auth_filter) { + /* Step 1: Check current authentication state */ + authenticate_cookie(); + + /* Step 2: Handle POST login attempts */ + if (ctx.env.request_method && + !strcmp(ctx.env.request_method, "POST")) + authenticate_post(); + + /* Step 3: Run the auth filter to decide access */ + cmd->fn(&ctx); +} +``` + +### `authenticate_cookie()` + +Opens the auth filter to check the current session cookie: + +```c +static void authenticate_cookie(void) +{ + /* Open auth filter with current request context */ + cgit_open_filter(ctx.cfg.auth_filter, + ctx.env.http_cookie, /* current cookies */ + ctx.env.request_method, /* GET/POST */ + ctx.env.query_string, /* full query */ + ctx.env.http_referer, /* referer header */ + ctx.env.path_info, /* request path */ + ctx.env.http_host, /* hostname */ + ctx.env.https ? "1" : "0", /* HTTPS flag */ + ctx.qry.repo, /* repository name */ + ctx.qry.page, /* page/command */ + ctx.env.http_accept, /* accept header */ + "cookie" /* authentication phase */ + ); + /* Read filter's response to determine auth state */ + ctx.env.authenticated = /* filter exit code */; + cgit_close_filter(ctx.cfg.auth_filter); +} +``` + +### `authenticate_post()` + +Handles login form submissions: + +```c +static void authenticate_post(void) +{ + /* Read POST body for credentials */ + /* Open auth filter with phase="post" */ + cgit_open_filter(ctx.cfg.auth_filter, + /* ... same 11 args ... */ + "post" /* authentication phase */ + ); + /* Filter processes credentials, may set cookies */ + cgit_close_filter(ctx.cfg.auth_filter); +} +``` + +### Authorization Check + +After authentication, the auth filter is called again before rendering each +page to determine if the authenticated user has access to the requested +repository and page: + +```c +static int open_auth_filter(const char *repo, const char *page) +{ + cgit_open_filter(ctx.cfg.auth_filter, + /* ... request context ... */ + "authorize" /* authorization phase */ + ); + int authorized = cgit_close_filter(ctx.cfg.auth_filter); + return authorized == 0; /* 0 = authorized */ +} +``` + +## Auth Filter Arguments + +The auth filter receives 12 arguments in total: + +| # | Argument | Description | +|---|----------|-------------| +| 1 | `filter_cmd` | The filter command itself | +| 2 | `http_cookie` | Raw `HTTP_COOKIE` header value | +| 3 | `request_method` | HTTP method (`GET`, `POST`) | +| 4 | `query_string` | Raw query string | +| 5 | `http_referer` | HTTP Referer header | +| 6 | `path_info` | PATH_INFO from CGI | +| 7 | `http_host` | Hostname | +| 8 | `https` | `"1"` if HTTPS, `"0"` if HTTP | +| 9 | `repo` | Repository URL | +| 10 | `page` | Page/command name | +| 11 | `http_accept` | HTTP Accept header | +| 12 | `phase` | `"cookie"`, `"post"`, or `"authorize"` | + +## Filter Phases + +### `cookie` Phase + +Called on every request. The filter should: +1. Read the session cookie from argument 2 +2. Validate the session +3. Return exit code 0 if authenticated, non-zero otherwise + +### `post` Phase + +Called when the request method is POST. The filter should: +1. Read POST body from stdin +2. Validate credentials +3. If valid, output a `Set-Cookie` header +4. Output a redirect response (302) + +### `authorize` Phase + +Called after authentication to check per-repository access. The filter +should: +1. Check if the authenticated user can access the requested repo/page +2. Return exit code 0 if authorized +3. Return non-zero to deny access (cgit will show an error page) + +## Filter Return Codes + +| Exit Code | Meaning | +|-----------|---------| +| 0 | Success (authenticated/authorized) | +| Non-zero | Failure (unauthenticated/unauthorized) | + +## Environment Variables + +The auth filter also has access to standard CGI environment variables: + +```c +struct cgit_environment { + const char *cgit_config; /* $CGIT_CONFIG */ + const char *http_host; /* $HTTP_HOST */ + const char *https; /* $HTTPS */ + const char *no_http; /* $NO_HTTP */ + const char *http_cookie; /* $HTTP_COOKIE */ + const char *request_method; /* $REQUEST_METHOD */ + const char *query_string; /* $QUERY_STRING */ + const char *http_referer; /* $HTTP_REFERER */ + const char *path_info; /* $PATH_INFO */ + const char *script_name; /* $SCRIPT_NAME */ + const char *server_name; /* $SERVER_NAME */ + const char *server_port; /* $SERVER_PORT */ + const char *http_accept; /* $HTTP_ACCEPT */ + int authenticated; /* set by auth filter */ +}; +``` + +## Shipped Auth Filter + +cgit ships a Lua-based hierarchical authentication filter: + +### `filters/simple-hierarchical-auth.lua` + +This filter implements path-based access control using a simple user +database and repository permission map. + +Features: +- Cookie-based session management +- Per-repository access control +- Hierarchical path matching +- Password hashing + +Usage: + +```ini +auth-filter=lua:/usr/lib/cgit/filters/simple-hierarchical-auth.lua +``` + +## Cache Interaction + +Authentication affects cache keys. The cache key includes the +authentication state and cookie: + +```c +static const char *cache_key(void) +{ + return fmt("%s?%s?%s?%s?%s", + ctx.qry.raw, + ctx.env.http_host, + ctx.env.https ? "1" : "0", + ctx.env.authenticated ? "1" : "0", + ctx.env.http_cookie ? ctx.env.http_cookie : ""); +} +``` + +This ensures that: +- Authenticated and unauthenticated users get separate cache entries +- Different authenticated users (different cookies) get separate entries +- The cache never leaks restricted content to unauthorized users + +## Security Considerations + +1. **HTTPS**: Always use HTTPS when authentication is enabled to protect + cookies and credentials in transit +2. **Cookie flags**: Auth filter scripts should set `Secure`, `HttpOnly`, + and `SameSite` cookie flags +3. **Session expiry**: Implement session timeouts in the auth filter +4. **Password storage**: Never store passwords in plain text; use bcrypt or + similar hashing +5. **CSRF protection**: The auth filter should implement CSRF tokens for + POST login forms +6. **Cache poisoning**: The cache key includes auth state, but ensure the + auth filter is deterministic for the same cookie + +## Disabling Authentication + +By default, no auth filter is configured and all repositories are publicly +accessible. To restrict access, set up the auth filter and optionally +combine with `strict-export` for file-based visibility control. + +## Example: Custom Auth Filter (Shell) + +```bash +#!/bin/bash +# Simple auth filter skeleton +PHASE="${12}" + +case "$PHASE" in + cookie) + COOKIE="$2" + if validate_session "$COOKIE"; then + exit 0 # authenticated + fi + exit 1 # not authenticated + ;; + post) + read -r POST_BODY + # Parse username/password from POST_BODY + # Validate credentials + # Set cookie header + echo "Status: 302 Found" + echo "Set-Cookie: session=TOKEN; HttpOnly; Secure" + echo "Location: $6" + echo + exit 0 + ;; + authorize) + REPO="$9" + # Check if current user can access $REPO + exit 0 # authorized + ;; +esac +``` diff --git a/docs/handbook/cgit/building.md b/docs/handbook/cgit/building.md new file mode 100644 index 0000000000..00f9e1244f --- /dev/null +++ b/docs/handbook/cgit/building.md @@ -0,0 +1,272 @@ +# cgit — Building + +## Prerequisites + +| Dependency | Required | Purpose | +|-----------|----------|---------| +| GCC or Clang | Yes | C compiler (C99) | +| GNU Make | Yes | Build system | +| OpenSSL (libcrypto) | Yes | SHA-1 hash implementation (`SHA1_HEADER = <openssl/sha.h>`) | +| zlib | Yes | Git object compression | +| libcurl | No | Not used — `NO_CURL=1` is passed by cgit.mk | +| Lua or LuaJIT | No | Lua filter support; auto-detected via pkg-config | +| asciidoc / a2x | No | Man page / HTML / PDF documentation generation | +| Python | No | Git's test harness (for `make test`) | + +## Build System Overview + +cgit uses a two-stage build that embeds itself within Git's build infrastructure: + +``` +cgit/Makefile + └── make -C git -f ../cgit.mk ../cgit + └── git/Makefile (included by cgit.mk) + └── Compile cgit objects + link against libgit.a +``` + +### Stage 1: Top-Level Makefile + +The top-level `Makefile` lives in `cgit/` and defines all user-configurable +variables: + +```makefile +CGIT_VERSION = 0.0.5-1-Project-Tick +CGIT_SCRIPT_NAME = cgit.cgi +CGIT_SCRIPT_PATH = /var/www/htdocs/cgit +CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH) +CGIT_CONFIG = /etc/cgitrc +CACHE_ROOT = /var/cache/cgit +prefix = /usr/local +libdir = $(prefix)/lib +filterdir = $(libdir)/cgit/filters +docdir = $(prefix)/share/doc/cgit +mandir = $(prefix)/share/man +SHA1_HEADER = <openssl/sha.h> +GIT_VER = 2.46.0 +GIT_URL = https://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.xz +``` + +The main `cgit` target delegates to: + +```makefile +cgit: + $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) -f ../cgit.mk ../cgit NO_CURL=1 +``` + +This enters the `git/` subdirectory and runs `cgit.mk` from there, prefixing +all cgit source paths with `../`. + +### Stage 2: cgit.mk + +`cgit.mk` is run inside the `git/` directory so it can `include Makefile` to +inherit Git's build variables (`CC`, `CFLAGS`, linker flags, OS detection via +`config.mak.uname`, etc.). + +Key sections: + +#### Version tracking + +```makefile +$(CGIT_PREFIX)VERSION: force-version + @cd $(CGIT_PREFIX) && '$(SHELL_PATH_SQ)' ./gen-version.sh "$(CGIT_VERSION)" +``` + +The `gen-version.sh` script writes a `VERSION` file that is included by the +build. Only `cgit.o` references `CGIT_VERSION`, so only that object is rebuilt +when the version changes. + +#### CGIT_CFLAGS + +```makefile +CGIT_CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"' +CGIT_CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"' +CGIT_CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"' +``` + +These compile-time constants are used in `cgit.c` as default values in +`prepare_context()`. + +#### Lua detection + +```makefile +LUA_PKGCONFIG := $(shell for pc in luajit lua lua5.2 lua5.1; do \ + $(PKG_CONFIG) --exists $$pc 2>/dev/null && echo $$pc && break; \ +done) +``` + +If Lua is found, its `--cflags` and `--libs` are appended to `CGIT_CFLAGS` and +`CGIT_LIBS`. If not found, `NO_LUA=YesPlease` is set and `-DNO_LUA` is added. + +#### Linux sendfile + +```makefile +ifeq ($(uname_S),Linux) + HAVE_LINUX_SENDFILE = YesPlease +endif + +ifdef HAVE_LINUX_SENDFILE + CGIT_CFLAGS += -DHAVE_LINUX_SENDFILE +endif +``` + +This enables the `sendfile()` syscall in `cache.c` for zero-copy writes from +cache files to stdout. + +#### Object files + +All cgit source files are listed explicitly: + +```makefile +CGIT_OBJ_NAMES += cgit.o cache.o cmd.o configfile.o filter.o html.o +CGIT_OBJ_NAMES += parsing.o scan-tree.o shared.o +CGIT_OBJ_NAMES += ui-atom.o ui-blame.o ui-blob.o ui-clone.o ui-commit.o +CGIT_OBJ_NAMES += ui-diff.o ui-log.o ui-patch.o ui-plain.o ui-refs.o +CGIT_OBJ_NAMES += ui-repolist.o ui-shared.o ui-snapshot.o ui-ssdiff.o +CGIT_OBJ_NAMES += ui-stats.o ui-summary.o ui-tag.o ui-tree.o +``` + +The prefixed paths (`CGIT_OBJS := $(addprefix $(CGIT_PREFIX),$(CGIT_OBJ_NAMES))`) +point back to the `cgit/` directory from inside `git/`. + +## Quick Build + +```bash +cd cgit + +# Download the vendored Git source (required on first build) +make get-git + +# Build cgit binary +make -j$(nproc) +``` + +The output is a single binary named `cgit` in the `cgit/` directory. + +## Build Variables Reference + +| Variable | Default | Description | +|----------|---------|-------------| +| `CGIT_VERSION` | `0.0.5-1-Project-Tick` | Compiled-in version string | +| `CGIT_SCRIPT_NAME` | `cgit.cgi` | Name of the installed CGI binary | +| `CGIT_SCRIPT_PATH` | `/var/www/htdocs/cgit` | CGI binary install directory | +| `CGIT_DATA_PATH` | `$(CGIT_SCRIPT_PATH)` | Static assets (CSS, JS, images) directory | +| `CGIT_CONFIG` | `/etc/cgitrc` | Default config file path (compiled in) | +| `CACHE_ROOT` | `/var/cache/cgit` | Default cache directory (compiled in) | +| `prefix` | `/usr/local` | Install prefix | +| `libdir` | `$(prefix)/lib` | Library directory | +| `filterdir` | `$(libdir)/cgit/filters` | Filter scripts install directory | +| `docdir` | `$(prefix)/share/doc/cgit` | Documentation directory | +| `mandir` | `$(prefix)/share/man` | Man page directory | +| `SHA1_HEADER` | `<openssl/sha.h>` | SHA-1 implementation header | +| `GIT_VER` | `2.46.0` | Git version to download and vendor | +| `GIT_URL` | `https://...git-$(GIT_VER).tar.xz` | Git source download URL | +| `NO_LUA` | (unset) | Set to any value to disable Lua support | +| `LUA_PKGCONFIG` | (auto-detected) | Explicit pkg-config name for Lua | +| `NO_C99_FORMAT` | (unset) | Define if your printf lacks `%zu`, `%lld` etc. | +| `HAVE_LINUX_SENDFILE` | (auto on Linux) | Enable `sendfile()` in cache | +| `V` | (unset) | Set to `1` for verbose build output | + +Overrides can be placed in a `cgit.conf` file (included by both `Makefile` and +`cgit.mk` via `-include cgit.conf`). + +## Installation + +```bash +make install # Install binary and static assets +make install-doc # Install man pages, HTML docs, PDF docs +make install-man # Man pages only +make install-html # HTML docs only +make install-pdf # PDF docs only +``` + +### Installed files + +| Path | Mode | Source | +|------|------|--------| +| `$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)` | 0755 | `cgit` binary | +| `$(CGIT_DATA_PATH)/cgit.css` | 0644 | Default stylesheet | +| `$(CGIT_DATA_PATH)/cgit.js` | 0644 | Client-side JavaScript | +| `$(CGIT_DATA_PATH)/cgit.png` | 0644 | Default logo | +| `$(CGIT_DATA_PATH)/favicon.ico` | 0644 | Default favicon | +| `$(CGIT_DATA_PATH)/robots.txt` | 0644 | Robots exclusion file | +| `$(filterdir)/*` | (varies) | Filter scripts from `filters/` | +| `$(mandir)/man5/cgitrc.5` | 0644 | Man page (if `install-man`) | + +## Make Targets + +| Target | Description | +|--------|-------------| +| `all` | Build the cgit binary (default) | +| `cgit` | Explicit build target | +| `test` | Build everything (`all` target on git) then run `tests/` | +| `install` | Install binary, CSS, JS, images, filters | +| `install-doc` | Install man pages + HTML + PDF | +| `install-man` | Man pages only | +| `install-html` | HTML docs only | +| `install-pdf` | PDF docs only | +| `clean` | Remove cgit objects, VERSION, CGIT-CFLAGS, tags | +| `cleanall` | `clean` + `make -C git clean` | +| `clean-doc` | Remove generated doc files | +| `get-git` | Download and extract Git source into `git/` | +| `tags` | Generate ctags for all `*.[ch]` files | +| `sparse` | Run `sparse` static analysis via cgit.mk | +| `uninstall` | Remove installed binary and assets | +| `uninstall-doc` | Remove installed documentation | + +## Documentation Generation + +Man pages are generated from `cgitrc.5.txt` using `asciidoc`/`a2x`: + +```makefile +MAN5_TXT = $(wildcard *.5.txt) +DOC_MAN5 = $(patsubst %.txt,%,$(MAN5_TXT)) +DOC_HTML = $(patsubst %.txt,%.html,$(MAN_TXT)) +DOC_PDF = $(patsubst %.txt,%.pdf,$(MAN_TXT)) + +%.5 : %.5.txt + a2x -f manpage $< + +$(DOC_HTML): %.html : %.txt + $(TXT_TO_HTML) -o $@+ $< && mv $@+ $@ + +$(DOC_PDF): %.pdf : %.txt + a2x -f pdf cgitrc.5.txt +``` + +## Cross-Compilation + +For cross-compiling (e.g. targeting MinGW on Linux): + +```bash +make CC=x86_64-w64-mingw32-gcc +``` + +The `toolchain-mingw32.cmake` file in the repository is for CMake-based +projects; cgit itself uses Make exclusively. + +## Customizing the Build + +Create a `cgit.conf` file alongside the Makefile: + +```makefile +# cgit.conf — local build overrides +CGIT_VERSION = 1.0.0-custom +CGIT_CONFIG = /usr/local/etc/cgitrc +CACHE_ROOT = /tmp/cgit-cache +NO_LUA = 1 +``` + +This file is `-include`d by both `Makefile` and `cgit.mk`, so it applies to +all build stages. + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| `make: *** No rule to make target 'git/Makefile'` | Run `make get-git` first | +| `lua.h: No such file or directory` | Install Lua dev package or set `NO_LUA=1` | +| `openssl/sha.h: No such file or directory` | Install `libssl-dev` / `openssl-devel` | +| `sendfile: undefined reference` | Set `HAVE_LINUX_SENDFILE=` (empty) on non-Linux | +| Build fails with `redefinition of 'struct cache_slot'` | Git's `cache.h` conflict — cgit uses `CGIT_CACHE_H` guard | +| `dlsym: symbol not found: write` | Lua filter's `write()` interposition requires `-ldl` (auto on Linux) | +| Version shows as `unknown` | Run `./gen-version.sh "$(CGIT_VERSION)"` or check `VERSION` file | diff --git a/docs/handbook/cgit/caching-system.md b/docs/handbook/cgit/caching-system.md new file mode 100644 index 0000000000..5d3b723ed5 --- /dev/null +++ b/docs/handbook/cgit/caching-system.md @@ -0,0 +1,287 @@ +# cgit — Caching System + +## Overview + +cgit implements a file-based output cache that stores the fully rendered +HTML/binary response for each unique request. The cache avoids regenerating +pages for repeated identical requests. When caching is disabled +(`cache-size=0`, the default), all output is written directly to `stdout`. + +Source files: `cache.c`, `cache.h`. + +## Cache Slot Structure + +Each cached response is represented by a `cache_slot`: + +```c +struct cache_slot { + const char *key; /* request identifier (URL-based) */ + int keylen; /* strlen(key) */ + int ttl; /* time-to-live in minutes */ + cache_fill_fn fn; /* callback to regenerate content */ + int cache_fd; /* fd for the cache file */ + int lock_fd; /* fd for the .lock file */ + const char *cache_name;/* path: cache_root/hash(key) */ + const char *lock_name; /* path: cache_name + ".lock" */ + int match; /* 1 if cache file matches key */ + struct stat cache_st; /* stat of the cache file */ + int bufsize; /* size of the header buffer */ + char buf[1024 + 4 * 20]; /* header: key + timestamps */ +}; +``` + +The `cache_fill_fn` typedef: + +```c +typedef void (*cache_fill_fn)(void *cbdata); +``` + +This callback is invoked to produce the page content when the cache needs +filling. The callback writes directly to `stdout`, which is redirected to the +lock file while cache filling is in progress. + +## Hash Function + +Cache file names are derived from the request key using the FNV-1 hash: + +```c +unsigned long hash_str(const char *str) +{ + unsigned long h = 0x811c9dc5; + unsigned char *s = (unsigned char *)str; + while (*s) { + h *= 0x01000193; + h ^= (unsigned long)*s++; + } + return h; +} +``` + +The resulting hash is formatted as `%lx` and joined with the configured +`cache-root` directory to produce the cache file path. The lock file is +the same path with `.lock` appended. + +## Slot Lifecycle + +A cache request goes through these phases, managed by `process_slot()`: + +### 1. Open (`open_slot`) + +Opens the cache file and reads the header. The header contains the original +key followed by creation and expiry timestamps. If the stored key matches the +current request key, `slot->match` is set to 1. + +```c +static int open_slot(struct cache_slot *slot) +{ + slot->cache_fd = open(slot->cache_name, O_RDONLY); + if (slot->cache_fd == -1) + return errno; + if (fstat(slot->cache_fd, &slot->cache_st)) + return errno; + /* read header into slot->buf */ + return 0; +} +``` + +### 2. Check Match + +If the file exists and the key matches, the code checks whether the entry +has expired based on the TTL: + +```c +static int is_expired(struct cache_slot *slot) +{ + if (slot->ttl < 0) + return 0; /* negative TTL = never expires */ + return slot->cache_st.st_mtime + slot->ttl * 60 < time(NULL); +} +``` + +A TTL of `-1` means the entry never expires (used for `cache-static-ttl`). + +### 3. Lock (`lock_slot`) + +Creates the `.lock` file with `O_WRONLY | O_CREAT | O_EXCL` and writes the +header containing the key and timestamps. If locking fails (another process +holds the lock), the stale cached content is served instead. + +```c +static int lock_slot(struct cache_slot *slot) +{ + slot->lock_fd = open(slot->lock_name, + O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); + if (slot->lock_fd == -1) + return errno; + /* write header: key + creation timestamp */ + return 0; +} +``` + +### 4. Fill (`fill_slot`) + +Redirects `stdout` to the lock file using `dup2()`, invokes the +`cache_fill_fn` callback to generate the page content, then restores `stdout`: + +```c +static int fill_slot(struct cache_slot *slot) +{ + /* save original stdout */ + /* dup2(slot->lock_fd, STDOUT_FILENO) */ + slot->fn(slot->cbdata); + /* restore original stdout */ + return 0; +} +``` + +### 5. Close and Rename + +After filling, the lock file is atomically renamed to the cache file: + +```c +if (rename(slot->lock_name, slot->cache_name)) + return errno; +``` + +This ensures readers never see a partially-written file. + +### 6. Print (`print_slot`) + +The cache file content (minus the header) is sent to `stdout`. On Linux, +`sendfile()` is used for zero-copy output: + +```c +static int print_slot(struct cache_slot *slot) +{ +#ifdef HAVE_LINUX_SENDFILE + off_t start = slot->keylen + 1; /* skip header */ + sendfile(STDOUT_FILENO, slot->cache_fd, &start, + slot->cache_st.st_size - start); +#else + /* fallback: read()/write() loop */ +#endif +} +``` + +## Process Slot State Machine + +`process_slot()` implements a state machine combining all phases: + +``` +START → open_slot() + ├── success + key match + not expired → print_slot() → DONE + ├── success + key match + expired → lock_slot() + │ ├── lock acquired → fill_slot() → close_slot() → open_slot() → print_slot() + │ └── lock failed → print_slot() (serve stale) + ├── success + key mismatch → lock_slot() + │ ├── lock acquired → fill_slot() → close_slot() → open_slot() → print_slot() + │ └── lock failed → fill_slot() (direct to stdout) + └── open failed → lock_slot() + ├── lock acquired → fill_slot() → close_slot() → open_slot() → print_slot() + └── lock failed → fill_slot() (direct to stdout, no cache) +``` + +## Public API + +```c +/* Process a request through the cache */ +extern int cache_process(int size, const char *path, const char *key, + int ttl, cache_fill_fn fn, void *cbdata); + +/* List all cache entries (for debugging/administration) */ +extern int cache_ls(const char *path); + +/* Hash a string using FNV-1 */ +extern unsigned long hash_str(const char *str); +``` + +### `cache_process()` + +Parameters: +- `size` — Maximum number of cache entries (from `cache-size`). If `0`, + caching is bypassed and `fn` is called directly. +- `path` — Cache root directory. +- `key` — Request identifier (derived from full URL + query string). +- `ttl` — Time-to-live in minutes. +- `fn` — Callback function that generates the page content. +- `cbdata` — Opaque data passed to the callback. + +### `cache_ls()` + +Scans the cache root directory and prints information about each cache entry +to `stdout`. Used for administrative inspection. + +## TTL Configuration Mapping + +Different page types have different TTLs: + +| Page Type | Config Directive | Default | Applied When | +|-----------|-----------------|---------|--------------| +| Repository list | `cache-root-ttl` | 5 min | `cmd->want_repo == 0` | +| Repo pages | `cache-repo-ttl` | 5 min | `cmd->want_repo == 1` and dynamic | +| Dynamic pages | `cache-dynamic-ttl` | 5 min | `cmd->want_vpath == 1` | +| Static content | `cache-static-ttl` | -1 (never) | SHA-referenced content | +| About pages | `cache-about-ttl` | 15 min | About/readme view | +| Snapshots | `cache-snapshot-ttl` | 5 min | Snapshot downloads | +| Scan results | `cache-scanrc-ttl` | 15 min | scan-path results | + +Static content uses a TTL of `-1` because SHA-addressed content is +immutable — a given commit/tree/blob hash always refers to the same data. + +## Cache Key Generation + +The cache key is built from the complete query context in `cgit.c`: + +```c +static const char *cache_key(void) +{ + return fmt("%s?%s?%s?%s?%s", + ctx.qry.raw, ctx.env.http_host, + ctx.env.https ? "1" : "0", + ctx.env.authenticated ? "1" : "0", + ctx.env.http_cookie ? ctx.env.http_cookie : ""); +} +``` + +The key captures: raw query string, hostname, HTTPS state, authentication +state, and cookies. This ensures that authenticated users get different +cache entries than unauthenticated users. + +## Concurrency + +The cache supports concurrent access from multiple CGI processes: + +1. **Atomic writes**: Content is written to a `.lock` file first, then + atomically renamed to the cache file. Readers never see partial content. +2. **Non-blocking locks**: If a lock is already held, the process either + serves stale cached content (if available) or generates content directly + to stdout without caching. +3. **No deadlocks**: Lock files are `O_EXCL`, not `flock()`. If a process + crashes while holding a lock, the stale `.lock` file remains. It is + typically cleaned up by the next successful writer. + +## Cache Directory Management + +The cache root directory (`cache-root`, default `/var/cache/cgit`) must be +writable by the web server user. Cache files are created with mode `0600` +(`S_IRUSR | S_IWUSR`). + +There is no built-in cache eviction. Old cache files persist until a new +request with the same hash replaces them. Administrators should set up +periodic cleanup (e.g., a cron job) to purge expired files: + +```bash +find /var/cache/cgit -type f -mmin +60 -delete +``` + +## Disabling the Cache + +Set `cache-size=0` (the default). When `size` is 0, `cache_process()` calls +the fill function directly, writing to stdout with no file I/O overhead: + +```c +if (!size) { + fn(cbdata); + return 0; +} +``` diff --git a/docs/handbook/cgit/code-style.md b/docs/handbook/cgit/code-style.md new file mode 100644 index 0000000000..d4059391dc --- /dev/null +++ b/docs/handbook/cgit/code-style.md @@ -0,0 +1,356 @@ +# cgit — Code Style and Conventions + +## Overview + +cgit follows C99 conventions with a style influenced by the Linux kernel and +Git project coding standards. This document describes the patterns, naming +conventions, and idioms used throughout the codebase. + +## Language Standard + +cgit is written in C99, compiled with: + +```makefile +CGIT_CFLAGS += -std=c99 +``` + +No C11 or GNU extensions are required, though some platform-specific features +(like `sendfile()` on Linux) are conditionally compiled. + +## Formatting + +### Indentation + +- Tabs for indentation (1 tab = 8 spaces display width, consistent with + Linux kernel/Git style) +- No spaces for indentation alignment + +### Braces + +K&R style (opening brace on same line): + +```c +if (condition) { + /* body */ +} else { + /* body */ +} + +static void function_name(int arg) +{ + /* function body */ +} +``` + +Functions place the opening brace on its own line. Control structures +(`if`, `for`, `while`, `switch`) keep it on the same line. + +### Line Length + +No strict limit, but lines generally stay under 80 characters. Long function +calls are broken across lines. + +## Naming Conventions + +### Functions + +Public API functions use the `cgit_` prefix: + +```c +void cgit_print_commit(const char *rev, const char *prefix); +void cgit_print_diff(const char *new_rev, const char *old_rev, ...); +struct cgit_repo *cgit_add_repo(const char *url); +struct cgit_repo *cgit_get_repoinfo(const char *url); +int cgit_parse_snapshots_mask(const char *str); +``` + +Static (file-local) functions use descriptive names without prefix: + +```c +static void config_cb(const char *name, const char *value); +static void querystring_cb(const char *name, const char *value); +static void process_request(void); +static int open_slot(struct cache_slot *slot); +``` + +### Types + +Struct types use `cgit_` prefix with snake_case: + +```c +struct cgit_context; +struct cgit_repo; +struct cgit_config; +struct cgit_query; +struct cgit_page; +struct cgit_environment; +struct cgit_cmd; +struct cgit_filter; +struct cgit_snapshot_format; +``` + +### Macros and Constants + +Uppercase with underscores: + +```c +#define ABOUT_FILTER 0 +#define COMMIT_FILTER 1 +#define SOURCE_FILTER 2 +#define EMAIL_FILTER 3 +#define AUTH_FILTER 4 +#define DIFF_UNIFIED 0 +#define DIFF_SSDIFF 1 +#define DIFF_STATONLY 2 +#define FMT_BUFS 8 +#define FMT_SIZE 8192 +``` + +### Variables + +Global variables use descriptive names: + +```c +struct cgit_context ctx; +struct cgit_repolist cgit_repolist; +const char *cgit_version; +``` + +## File Organization + +### Header Files + +Each module has a corresponding header file with include guards: + +```c +#ifndef UI_DIFF_H +#define UI_DIFF_H + +extern void cgit_print_diff(const char *new_rev, const char *old_rev, + const char *prefix, int show_ctrls, int raw); +extern void cgit_print_diffstat(const struct object_id *old, + const struct object_id *new, + const char *prefix); + +#endif /* UI_DIFF_H */ +``` + +### Source Files + +Typical source file structure: + +1. License header (if present) +2. Include directives +3. Static (file-local) variables +4. Static helper functions +5. Public API functions + +### Module Pattern + +UI modules follow a consistent pattern with `ui-*.c` / `ui-*.h` pairs: + +```c +/* ui-example.c */ +#include "cgit.h" +#include "ui-example.h" +#include "html.h" +#include "ui-shared.h" + +static void helper_function(void) +{ + /* ... */ +} + +void cgit_print_example(void) +{ + /* main entry point */ +} +``` + +## Common Patterns + +### Global Context + +cgit uses a single global `struct cgit_context ctx` variable that holds all +request state. Functions access it directly rather than passing it as a +parameter: + +```c +/* Access global context directly */ +if (ctx.repo && ctx.repo->enable_blame) + cgit_print_blame(); + +/* Not: cgit_print_blame(&ctx) */ +``` + +### Callback Functions + +Configuration and query parsing use callback function pointers: + +```c +typedef void (*configfile_value_fn)(const char *name, const char *value); +typedef void (*filepair_fn)(struct diff_filepair *pair); +typedef void (*linediff_fn)(char *line, int len); +typedef void (*cache_fill_fn)(void *cbdata); +``` + +### String Formatting + +The `fmt()` ring buffer is used for temporary string construction: + +```c +const char *url = fmt("%s/%s/", ctx.cfg.virtual_root, repo->url); +html_attr(url); +``` + +Never store `fmt()` results long-term — use `fmtalloc()` or `xstrdup()`. + +### NULL Checks + +Functions generally check for NULL pointers at the start: + +```c +void cgit_print_blob(const char *hex, const char *path, + const char *head, int file_only) +{ + if (!hex && !path) { + cgit_print_error_page(400, "Bad request", + "Need either hex or path"); + return; + } + /* ... */ +} +``` + +### Memory Management + +cgit uses Git's `xmalloc` / `xstrdup` / `xrealloc` wrappers that die on +allocation failure: + +```c +char *name = xstrdup(value); +repo = xrealloc(repo, new_size); +``` + +No explicit `free()` calls in most paths — the CGI process exits after each +request, and the OS reclaims all memory. + +### Boolean as Int + +Boolean values are represented as `int` (0 or 1), consistent with C99 +convention before `_Bool`: + +```c +int enable_blame; +int enable_commit_graph; +int binary; +int match; +``` + +### Typedef Avoidance + +Structs are generally not typedef'd — they use the `struct` keyword +explicitly: + +```c +struct cgit_repo *repo; +struct cache_slot slot; +``` + +Exception: function pointer typedefs are used for callbacks: + +```c +typedef void (*configfile_value_fn)(const char *name, const char *value); +``` + +## Error Handling + +### `die()` for Fatal Errors + +Unrecoverable errors use Git's `die()`: + +```c +if (!ctx.repo) + die("no repository"); +``` + +### Error Pages for User Errors + +User-facing errors use the error page function: + +```c +cgit_print_error_page(404, "Not Found", + "No repository found for '%s'", + ctx.qry.repo); +``` + +### Return Codes + +Functions that can fail return int (0 = success, non-zero = error): + +```c +static int open_slot(struct cache_slot *slot) +{ + slot->cache_fd = open(slot->cache_name, O_RDONLY); + if (slot->cache_fd == -1) + return errno; + return 0; +} +``` + +## Preprocessor Usage + +Conditional compilation for platform features: + +```c +#ifdef HAVE_LINUX_SENDFILE + sendfile(STDOUT_FILENO, slot->cache_fd, &off, size); +#else + /* read/write fallback */ +#endif + +#ifdef HAVE_LUA + /* Lua filter support */ +#endif +``` + +## Git Library Integration + +cgit includes Git as a library. It uses Git's internal APIs directly: + +```c +#include "git/cache.h" +#include "git/object.h" +#include "git/commit.h" +#include "git/diff.h" +#include "git/revision.h" +#include "git/archive.h" +``` + +Functions from Git's library are called without wrapper layers: + +```c +struct commit *commit = lookup_commit_reference(&oid); +struct tree *tree = parse_tree_indirect(&oid); +init_revisions(&rev, NULL); +``` + +## Documentation + +- Code comments are used sparingly, mainly for non-obvious logic +- No Doxygen or similar documentation generators are used +- Function documentation is in the header files as prototypes with + descriptive parameter names +- The `cgitrc.5.txt` file provides user-facing documentation in + man page format + +## Commit Messages + +Commit messages follow the standard Git format: + +``` +subject: brief description (50 chars or less) + +Extended description wrapping at 72 characters. Explain what and why, +not how. +``` diff --git a/docs/handbook/cgit/configuration.md b/docs/handbook/cgit/configuration.md new file mode 100644 index 0000000000..afc29fce07 --- /dev/null +++ b/docs/handbook/cgit/configuration.md @@ -0,0 +1,351 @@ +# cgit — Configuration Reference + +## Configuration File + +Default location: `/etc/cgitrc` (compiled in as `CGIT_CONFIG`). Override at +runtime by setting the `$CGIT_CONFIG` environment variable. + +## File Format + +The configuration file uses a simple `name=value` format, parsed by +`parse_configfile()` in `configfile.c`. Key rules: + +- Lines starting with `#` or `;` are comments +- Leading whitespace on lines is skipped +- No quoting mechanism — the value is everything after the `=` to end of line +- Empty lines are ignored +- Nesting depth for `include=` directives is limited to 8 levels + +```c +int parse_configfile(const char *filename, configfile_value_fn fn) +{ + static int nesting; + /* ... */ + if (nesting > 8) + return -1; + /* ... */ + while (read_config_line(f, &name, &value)) + fn(name.buf, value.buf); + /* ... */ +} +``` + +## Global Directives + +All global directives are processed by `config_cb()` in `cgit.c`. When a +directive is encountered, the value is stored in the corresponding +`ctx.cfg.*` field. + +### Site Identity + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `root-title` | `"Git repository browser"` | string | HTML page title for the index page | +| `root-desc` | `"a fast webinterface for the git dscm"` | string | Subtitle text on the index page | +| `root-readme` | (none) | path | Path to a file rendered on the site about page | +| `root-coc` | (none) | path | Path to Code of Conduct file | +| `root-cla` | (none) | path | Path to Contributor License Agreement file | +| `root-homepage` | (none) | URL | External homepage URL | +| `root-homepage-title` | (none) | string | Title text for the homepage link | +| `root-link` | (none) | string | `label\|url` pairs for navigation links (can repeat) | +| `logo` | `"/cgit.png"` | URL | Path to the site logo image | +| `logo-link` | (none) | URL | URL the logo links to | +| `favicon` | `"/favicon.ico"` | URL | Path to the favicon | +| `css` | (none) | URL | Stylesheet URL (can repeat for multiple stylesheets) | +| `js` | (none) | URL | JavaScript URL (can repeat) | +| `header` | (none) | path | File included at the top of every page | +| `footer` | (none) | path | File included at the bottom of every page | +| `head-include` | (none) | path | File included in the HTML `<head>` | +| `robots` | `"index, nofollow"` | string | Content for `<meta name="robots">` | + +### URL Configuration + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `virtual-root` | (none) | path | Base URL path when using URL rewriting (always ends with `/`) | +| `script-name` | `CGIT_SCRIPT_NAME` | path | CGI script name (from `$SCRIPT_NAME` env var) | +| `clone-prefix` | (none) | string | Prefix for clone URLs when auto-generating | +| `clone-url` | (none) | string | Clone URL template (`$CGIT_REPO_URL` expanded) | + +When `virtual-root` is set, URLs use path-based routing: +`/cgit/repo/log/path`. Without it, query-string routing is used: +`?url=repo/log/path`. + +### Feature Flags + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `enable-http-clone` | `1` | int | Allow HTTP clone operations (HEAD, info/refs, objects/) | +| `enable-index-links` | `0` | int | Show log/tree/commit links on the repo index page | +| `enable-index-owner` | `1` | int | Show the Owner column on the repo index page | +| `enable-blame` | `0` | int | Enable blame view for all repos | +| `enable-commit-graph` | `0` | int | Show ASCII commit graph in log view | +| `enable-log-filecount` | `0` | int | Show changed-file count in log view | +| `enable-log-linecount` | `0` | int | Show added/removed line counts in log | +| `enable-remote-branches` | `0` | int | Display remote tracking branches | +| `enable-subject-links` | `0` | int | Show parent commit subjects instead of hashes | +| `enable-html-serving` | `0` | int | Serve HTML files as-is from plain view | +| `enable-subtree` | `0` | int | Detect and display git-subtree directories | +| `enable-tree-linenumbers` | `1` | int | Show line numbers in file/blob view | +| `enable-git-config` | `0` | int | Read `gitweb.*` and `cgit.*` from repo's git config | +| `enable-filter-overrides` | `0` | int | Allow repos to override global filters | +| `enable-follow-links` | `0` | int | Show "follow" links in log view for renames | +| `embedded` | `0` | int | Omit HTML boilerplate for embedding in another page | +| `noheader` | `0` | int | Suppress the page header | +| `noplainemail` | `0` | int | Hide email addresses in output | +| `local-time` | `0` | int | Display times in local timezone instead of UTC | + +### Limits + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `max-repo-count` | `50` | int | Repos per page on the index (≤0 → unlimited) | +| `max-commit-count` | `50` | int | Commits per page in log view | +| `max-message-length` | `80` | int | Truncate commit subject at this length | +| `max-repodesc-length` | `80` | int | Truncate repo description at this length | +| `max-blob-size` | `0` | int (KB) | Max blob size to display (0 = unlimited) | +| `max-stats` | `0` | int | Stats period (0=disabled, 1=week, 2=month, 3=quarter, 4=year) | +| `max-atom-items` | `10` | int | Number of entries in Atom feeds | +| `max-subtree-commits` | `2000` | int | Max commits to scan for subtree trailers | +| `renamelimit` | `-1` | int | Diff rename detection limit (-1 = Git default) | + +### Caching + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `cache-size` | `0` | int | Number of cache entries (0 = disabled) | +| `cache-root` | `CGIT_CACHE_ROOT` | path | Directory for cache files | +| `cache-root-ttl` | `5` | int (min) | TTL for repo-list pages | +| `cache-repo-ttl` | `5` | int (min) | TTL for repo-specific pages | +| `cache-dynamic-ttl` | `5` | int (min) | TTL for dynamic content | +| `cache-static-ttl` | `-1` | int (min) | TTL for static content (-1 = forever) | +| `cache-about-ttl` | `15` | int (min) | TTL for about/readme pages | +| `cache-snapshot-ttl` | `5` | int (min) | TTL for snapshot pages | +| `cache-scanrc-ttl` | `15` | int (min) | TTL for cached scan-path results | + +### Sorting + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `case-sensitive-sort` | `1` | int | Case-sensitive repo name sorting | +| `section-sort` | `1` | int | Sort sections alphabetically | +| `section-from-path` | `0` | int | Derive section name from path depth (>0 = from start, <0 = from end) | +| `repository-sort` | `"name"` | string | Default sort field for repo list | +| `branch-sort` | `0` | int | Branch sort: 0=name, 1=age | +| `commit-sort` | `0` | int | Commit sort: 0=default, 1=date, 2=topo | + +### Snapshots + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `snapshots` | (none) | string | Space-separated list of enabled formats: `.tar` `.tar.gz` `.tar.bz2` `.tar.lz` `.tar.xz` `.tar.zst` `.zip`. Also accepts `all`. | + +### Filters + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `about-filter` | (none) | filter | Filter for rendering README/about content | +| `source-filter` | (none) | filter | Filter for syntax highlighting source code | +| `commit-filter` | (none) | filter | Filter for commit messages | +| `email-filter` | (none) | filter | Filter for email display (2 args: email, page) | +| `owner-filter` | (none) | filter | Filter for owner display | +| `auth-filter` | (none) | filter | Authentication filter (12 args) | + +Filter values use the format `type:command`: +- `exec:/path/to/script` — external process filter +- `lua:/path/to/script.lua` — Lua script filter +- Plain path without prefix defaults to `exec` + +### Display + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `summary-branches` | `10` | int | Branches shown on summary page | +| `summary-tags` | `10` | int | Tags shown on summary page | +| `summary-log` | `10` | int | Log entries shown on summary page | +| `side-by-side-diffs` | `0` | int | Default to side-by-side diff view | +| `remove-suffix` | `0` | int | Remove `.git` suffix from repo URLs | +| `scan-hidden-path` | `0` | int | Include hidden dirs when scanning | + +### Miscellaneous + +| Directive | Default | Type | Description | +|-----------|---------|------|-------------| +| `agefile` | `"info/web/last-modified"` | path | File in repo checked for modification time | +| `mimetype-file` | (none) | path | Apache-style mime.types file | +| `mimetype.<ext>` | (none) | string | MIME type for a file extension | +| `module-link` | (none) | URL | URL template for submodule links | +| `strict-export` | (none) | path | Only export repos containing this file | +| `project-list` | (none) | path | File listing project directories for `scan-path` | +| `scan-path` | (none) | path | Directory to scan for git repositories | +| `readme` | (none) | string | Default README file spec (can repeat) | +| `include` | (none) | path | Include another config file | + +## Repository Directives + +Repository configuration begins with `repo.url=` which creates a new +repository entry via `cgit_add_repo()`. Subsequent `repo.*` directives +modify the most recently created repository via `repo_config()` in `cgit.c`. + +| Directive | Description | +|-----------|-------------| +| `repo.url` | Repository URL path (triggers new repo creation) | +| `repo.path` | Filesystem path to the git repository | +| `repo.name` | Display name | +| `repo.basename` | Override for basename derivation | +| `repo.desc` | Repository description | +| `repo.owner` | Repository owner name | +| `repo.homepage` | Project homepage URL | +| `repo.defbranch` | Default branch name | +| `repo.section` | Section heading for grouped display | +| `repo.clone-url` | Clone URL (overrides global) | +| `repo.readme` | README file spec (`[ref:]path`, can repeat) | +| `repo.logo` | Per-repo logo URL | +| `repo.logo-link` | Per-repo logo link URL | +| `repo.extra-head-content` | Extra HTML for `<head>` | +| `repo.snapshots` | Snapshot format mask (space-separated suffixes) | +| `repo.snapshot-prefix` | Prefix for snapshot filenames | +| `repo.enable-blame` | Override global enable-blame | +| `repo.enable-commit-graph` | Override global enable-commit-graph | +| `repo.enable-log-filecount` | Override global enable-log-filecount | +| `repo.enable-log-linecount` | Override global enable-log-linecount | +| `repo.enable-remote-branches` | Override global enable-remote-branches | +| `repo.enable-subject-links` | Override global enable-subject-links | +| `repo.enable-html-serving` | Override global enable-html-serving | +| `repo.enable-subtree` | Override global enable-subtree | +| `repo.max-stats` | Override global max-stats | +| `repo.max-subtree-commits` | Override global max-subtree-commits | +| `repo.branch-sort` | `"age"` or `"name"` | +| `repo.commit-sort` | `"date"` or `"topo"` | +| `repo.module-link` | Submodule URL template | +| `repo.module-link.<submodule>` | Per-submodule URL | +| `repo.badge` | Badge entry: `url\|imgurl` or just `imgurl` (can repeat) | +| `repo.hide` | `1` = hide from listing (still accessible by URL) | +| `repo.ignore` | `1` = completely ignore this repository | + +### Filter overrides (require `enable-filter-overrides=1`) + +| Directive | Description | +|-----------|-------------| +| `repo.about-filter` | Per-repo about filter | +| `repo.commit-filter` | Per-repo commit filter | +| `repo.source-filter` | Per-repo source filter | +| `repo.email-filter` | Per-repo email filter | +| `repo.owner-filter` | Per-repo owner filter | + +## Repository Defaults + +When a new repository is created by `cgit_add_repo()`, it inherits all global +defaults from `ctx.cfg`: + +```c +ret->section = ctx.cfg.section; +ret->snapshots = ctx.cfg.snapshots; +ret->enable_blame = ctx.cfg.enable_blame; +ret->enable_commit_graph = ctx.cfg.enable_commit_graph; +ret->enable_log_filecount = ctx.cfg.enable_log_filecount; +ret->enable_log_linecount = ctx.cfg.enable_log_linecount; +ret->enable_remote_branches = ctx.cfg.enable_remote_branches; +ret->enable_subject_links = ctx.cfg.enable_subject_links; +ret->enable_html_serving = ctx.cfg.enable_html_serving; +ret->enable_subtree = ctx.cfg.enable_subtree; +ret->max_stats = ctx.cfg.max_stats; +ret->max_subtree_commits = ctx.cfg.max_subtree_commits; +ret->branch_sort = ctx.cfg.branch_sort; +ret->commit_sort = ctx.cfg.commit_sort; +ret->module_link = ctx.cfg.module_link; +ret->readme = ctx.cfg.readme; +ret->about_filter = ctx.cfg.about_filter; +ret->commit_filter = ctx.cfg.commit_filter; +ret->source_filter = ctx.cfg.source_filter; +ret->email_filter = ctx.cfg.email_filter; +ret->owner_filter = ctx.cfg.owner_filter; +ret->clone_url = ctx.cfg.clone_url; +``` + +This means global directives should appear *before* `repo.url=` entries, since +they set the defaults for subsequently defined repositories. + +## Git Config Integration + +When `enable-git-config=1`, the `scan-tree` scanner reads each repository's +`.git/config` and maps gitweb-compatible directives: + +```c +if (!strcmp(key, "gitweb.owner")) + config_fn(repo, "owner", value); +else if (!strcmp(key, "gitweb.description")) + config_fn(repo, "desc", value); +else if (!strcmp(key, "gitweb.category")) + config_fn(repo, "section", value); +else if (!strcmp(key, "gitweb.homepage")) + config_fn(repo, "homepage", value); +else if (skip_prefix(key, "cgit.", &name)) + config_fn(repo, name, value); +``` + +Any `cgit.*` key in the git config is passed directly to the repo config +handler, allowing per-repo settings without modifying the global cgitrc. + +## README File Spec Format + +README directives support three forms: + +| Format | Meaning | +|--------|---------| +| `path` | File on disk, relative to repo path | +| `/absolute/path` | File on disk, absolute | +| `ref:path` | File tracked in the git repository at the given ref | +| `:path` | File tracked in the default branch or query head | + +Multiple `readme` directives can be specified. cgit tries each in order and +uses the first one found (checked via `cgit_ref_path_exists()` for tracked +files, or `access(R_OK)` for disk files). + +## Macro Expansion + +The `expand_macros()` function (in `shared.c`) performs environment variable +substitution in certain directive values (`cache-root`, `scan-path`, +`project-list`, `include`). A `$VARNAME` or `${VARNAME}` in the value is +replaced with the corresponding environment variable. + +## Example Configuration + +```ini +# Site settings +root-title=Project Tick Git +root-desc=Source code for Project Tick +logo=/cgit/cgit.png +css=/cgit/cgit.css +virtual-root=/cgit/ + +# Features +enable-commit-graph=1 +enable-blame=1 +enable-http-clone=1 +enable-index-links=1 +snapshots=tar.gz tar.xz zip +max-stats=quarter + +# Caching +cache-size=1000 +cache-root=/var/cache/cgit + +# Filters +source-filter=exec:/usr/lib/cgit/filters/syntax-highlighting.py +about-filter=exec:/usr/lib/cgit/filters/about-formatting.sh + +# Scanning +scan-path=/srv/git/ +section-from-path=1 + +# Or manual repo definitions: +repo.url=myproject +repo.path=/srv/git/myproject.git +repo.desc=My awesome project +repo.owner=Alice +repo.readme=master:README.md +repo.clone-url=https://git.example.com/myproject.git +repo.snapshots=tar.gz zip +repo.badge=https://ci.example.com/badge.svg|https://ci.example.com/ +``` diff --git a/docs/handbook/cgit/css-theming.md b/docs/handbook/cgit/css-theming.md new file mode 100644 index 0000000000..0a7b404595 --- /dev/null +++ b/docs/handbook/cgit/css-theming.md @@ -0,0 +1,522 @@ +# cgit — CSS Theming + +## Overview + +cgit ships with a comprehensive CSS stylesheet (`cgit.css`) that controls +the visual appearance of all pages. The stylesheet is designed with a light +color scheme and semantic CSS classes that map directly to cgit's HTML +structure. + +Source file: `cgit.css` (~450 lines). + +## Loading Stylesheets + +CSS files are specified via the `css` configuration directive: + +```ini +css=/cgit/cgit.css +``` + +Multiple stylesheets can be loaded by repeating the directive: + +```ini +css=/cgit/cgit.css +css=/cgit/custom.css +``` + +Stylesheets are included in document order in the `<head>` section via +`cgit_print_docstart()` in `ui-shared.c`. + +## Page Structure + +The HTML layout uses this basic structure: + +```html +<body> + <div id='cgit'> + <table id='header'>...</table> <!-- site header with logo --> + <table id='navigation'>...</table> <!-- tab navigation --> + <div id='content'> <!-- page content --> + <!-- page-specific content --> + </div> + <div class='footer'>...</div> <!-- footer --> + </div> +</body> +``` + +## Base Styles + +### Body and Layout + +```css +body { + font-family: sans-serif; + font-size: 11px; + color: #000; + background: white; + padding: 4px; +} + +div#cgit { + padding: 0; + margin: 0; + font-family: monospace; + font-size: 12px; +} +``` + +### Header + +```css +table#header { + width: 100%; + margin-bottom: 1em; +} + +table#header td.logo { + /* logo cell */ +} + +table#header td.main { + font-size: 250%; + font-weight: bold; + vertical-align: bottom; + padding-left: 10px; +} + +table#header td.sub { + color: #999; + font-size: 75%; + vertical-align: top; + padding-left: 10px; +} +``` + +### Navigation Tabs + +```css +table#navigation { + width: 100%; +} + +table#navigation a { + padding: 2px 6px; + color: #000; + text-decoration: none; +} + +table#navigation a:hover { + color: #00f; +} +``` + +## Content Areas + +### Repository List + +```css +table.list { + border-collapse: collapse; + border: solid 1px #aaa; + width: 100%; +} + +table.list th { + text-align: left; + font-weight: bold; + background: #ddd; + border-bottom: solid 1px #aaa; + padding: 2px 4px; +} + +table.list td { + padding: 2px 4px; + border: none; +} + +table.list tr:hover { + background: #eee; +} + +table.list td a { + color: #00f; + text-decoration: none; +} + +table.list td a:hover { + text-decoration: underline; +} +``` + +### Sections + +```css +div.section-header { + background: #eee; + border: solid 1px #ddd; + padding: 2px 4px; + font-weight: bold; + margin-top: 1em; +} +``` + +## Diff Styles + +### Diffstat + +```css +table.diffstat { + border-collapse: collapse; + border: solid 1px #aaa; +} + +table.diffstat td { + padding: 1px 4px; + border: none; +} + +table.diffstat td.mode { + font-weight: bold; + /* status indicator: A/M/D/R */ +} + +table.diffstat td.graph { + width: 500px; +} + +table.diffstat td.graph span.add { + background: #5f5; + /* green bar for additions */ +} + +table.diffstat td.graph span.rem { + background: #f55; + /* red bar for deletions */ +} + +table.diffstat .total { + font-weight: bold; + text-align: center; +} +``` + +### Unified Diff + +```css +table.diff { + width: 100%; +} + +table.diff td div.head { + font-weight: bold; + margin-top: 1em; + color: #000; +} + +table.diff td div.hunk { + color: #009; + /* hunk header @@ ... @@ */ +} + +table.diff td div.add { + color: green; + background: #dfd; +} + +table.diff td div.del { + color: red; + background: #fdd; +} +``` + +### Side-by-Side Diff + +```css +table.ssdiff { + width: 100%; +} + +table.ssdiff td { + font-family: monospace; + font-size: 12px; + padding: 1px 4px; + vertical-align: top; +} + +table.ssdiff td.lineno { + text-align: right; + width: 3em; + background: #eee; + color: #999; +} + +table.ssdiff td.add { + background: #dfd; +} + +table.ssdiff td.del { + background: #fdd; +} + +table.ssdiff td.changed { + background: #ffc; +} + +table.ssdiff span.add { + background: #afa; + font-weight: bold; +} + +table.ssdiff span.del { + background: #faa; + font-weight: bold; +} +``` + +## Blob/Tree View + +```css +table.blob { + border-collapse: collapse; + width: 100%; +} + +table.blob td { + font-family: monospace; + font-size: 12px; + padding: 0 4px; + vertical-align: top; +} + +table.blob td.linenumbers { + text-align: right; + color: #999; + background: #eee; + width: 3em; + border-right: solid 1px #ddd; +} + +table.blob td.lines { + white-space: pre; +} +``` + +### Tree Listing + +```css +table.list td.ls-mode { + font-family: monospace; + width: 10em; +} + +table.list td.ls-size { + text-align: right; + width: 5em; +} +``` + +## Commit View + +```css +table.commit-info { + border-collapse: collapse; + border: solid 1px #aaa; + margin-bottom: 1em; +} + +table.commit-info th { + text-align: left; + font-weight: bold; + padding: 2px 6px; + vertical-align: top; +} + +table.commit-info td { + padding: 2px 6px; +} + +div.commit-subject { + font-weight: bold; + font-size: 125%; + margin: 1em 0 0.5em; +} + +div.commit-msg { + white-space: pre; + font-family: monospace; +} + +div.notes-header { + font-weight: bold; + padding-top: 1em; +} + +div.notes { + white-space: pre; + font-family: monospace; + border-left: solid 3px #dd5; + padding: 0.5em; + background: #ffe; +} +``` + +## Log View + +```css +div.commit-graph { + font-family: monospace; + white-space: pre; + color: #000; +} + +/* Column colors for commit graph */ +.column1 { color: #a00; } +.column2 { color: #0a0; } +.column3 { color: #00a; } +.column4 { color: #aa0; } +.column5 { color: #0aa; } +.column6 { color: #a0a; } +``` + +## Stats View + +```css +table.stats { + border-collapse: collapse; + border: solid 1px #aaa; +} + +table.stats th { + text-align: left; + padding: 2px 6px; + background: #ddd; +} + +table.stats td { + padding: 2px 6px; +} + +div.stats-graph { + /* bar chart container */ +} +``` + +## Form Elements + +```css +div.cgit-panel { + float: right; + margin: 0 0 0.5em 0.5em; + padding: 4px; + border: solid 1px #aaa; + background: #eee; +} + +div.cgit-panel b { + display: block; + margin-bottom: 2px; +} + +div.cgit-panel select, +div.cgit-panel input { + font-size: 11px; +} +``` + +## Customization Strategies + +### Method 1: Override Stylesheet + +Create a custom CSS file that overrides specific rules: + +```css +/* /cgit/custom.css */ +body { + background: #1a1a2e; + color: #e0e0e0; +} + +div#cgit { + background: #16213e; +} + +table.list th { + background: #0f3460; + color: #e0e0e0; +} +``` + +```ini +css=/cgit/cgit.css +css=/cgit/custom.css +``` + +### Method 2: Replace Stylesheet + +Replace the default stylesheet entirely: + +```ini +css=/cgit/mytheme.css +``` + +### Method 3: head-include + +Inject inline styles via the `head-include` directive: + +```ini +head-include=/etc/cgit/extra-head.html +``` + +```html +<!-- /etc/cgit/extra-head.html --> +<style> + body { background: #f0f0f0; } +</style> +``` + +## CSS Classes Reference + +### Layout Classes + +| Class/ID | Element | Description | +|----------|---------|-------------| +| `#cgit` | div | Main container | +| `#header` | table | Site header | +| `#navigation` | table | Tab navigation | +| `#content` | div | Page content area | +| `.footer` | div | Page footer | + +### Content Classes + +| Class | Element | Description | +|-------|---------|-------------| +| `.list` | table | Data listing (repos, files, refs) | +| `.blob` | table | File content display | +| `.diff` | table | Unified diff | +| `.ssdiff` | table | Side-by-side diff | +| `.diffstat` | table | Diff statistics | +| `.commit-info` | table | Commit metadata | +| `.stats` | table | Statistics data | +| `.cgit-panel` | div | Control panel | + +### Diff Classes + +| Class | Element | Description | +|-------|---------|-------------| +| `.add` | div/span | Added lines/chars | +| `.del` | div/span | Deleted lines/chars | +| `.hunk` | div | Hunk header | +| `.ctx` | div | Context lines | +| `.head` | div | File header | +| `.changed` | td | Modified line (ssdiff) | +| `.lineno` | td | Line number column | + +### Status Classes + +| Class | Description | +|-------|-------------| +| `.upd` | Modified file | +| `.add` | Added file | +| `.del` | Deleted file | +| `.mode` | File mode indicator | +| `.graph` | Graph bar container | diff --git a/docs/handbook/cgit/deployment.md b/docs/handbook/cgit/deployment.md new file mode 100644 index 0000000000..8c991726af --- /dev/null +++ b/docs/handbook/cgit/deployment.md @@ -0,0 +1,369 @@ +# cgit — Deployment Guide + +## Overview + +cgit runs as a CGI application under a web server. This guide covers +compilation, installation, web server configuration, and production tuning. + +## Prerequisites + +Build dependencies: +- GCC or Clang (C99 compiler) +- GNU Make +- OpenSSL or compatible TLS library (for libgit HTTPS) +- zlib (for git object decompression) +- Optional: Lua or LuaJIT (for Lua filters) +- Optional: pkg-config (for Lua detection) + +Runtime dependencies: +- A CGI-capable web server (Apache, Nginx+fcgiwrap, lighttpd) +- Git repositories on the filesystem + +## Building + +```bash +# Clone/download the source +cd cgit/ + +# Build with defaults +make + +# Or with custom settings +make prefix=/usr CGIT_SCRIPT_PATH=/var/www/cgi-bin \ + CGIT_CONFIG=/etc/cgitrc CACHE_ROOT=/var/cache/cgit + +# Install +make install +``` + +### Build Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `prefix` | `/usr/local` | Installation prefix | +| `CGIT_SCRIPT_PATH` | `$(prefix)/lib/cgit` | CGI binary directory | +| `CGIT_DATA_PATH` | `$(prefix)/share/cgit` | Static files (CSS, images) | +| `CGIT_CONFIG` | `/etc/cgitrc` | Default config file path | +| `CACHE_ROOT` | `/var/cache/cgit` | Default cache directory | +| `CGIT_SCRIPT_NAME` | `"/"` | Default CGI script name | +| `NO_LUA` | (unset) | Set to 1 to disable Lua | + +### Installed Files + +``` +$(CGIT_SCRIPT_PATH)/cgit.cgi # CGI binary +$(CGIT_DATA_PATH)/cgit.css # Stylesheet +$(CGIT_DATA_PATH)/cgit.js # JavaScript +$(CGIT_DATA_PATH)/cgit.png # Logo image +$(CGIT_DATA_PATH)/robots.txt # Robots exclusion file +``` + +## Apache Configuration + +### CGI Module + +```apache +# Enable CGI +LoadModule cgi_module modules/mod_cgi.so + +# Basic CGI setup +ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/ +Alias /cgit-data/ /usr/share/cgit/ + +<Directory "/usr/lib/cgit/"> + AllowOverride None + Options +ExecCGI + Require all granted +</Directory> + +<Directory "/usr/share/cgit/"> + AllowOverride None + Require all granted +</Directory> +``` + +### URL Rewriting (Clean URLs) + +```apache +# Enable clean URLs via mod_rewrite +RewriteEngine On +RewriteRule ^/cgit/(.*)$ /usr/lib/cgit/cgit.cgi/$1 [PT] +``` + +With corresponding cgitrc: + +```ini +virtual-root=/cgit/ +css=/cgit-data/cgit.css +logo=/cgit-data/cgit.png +``` + +## Nginx Configuration + +Nginx does not support CGI natively. Use `fcgiwrap` or `spawn-fcgi`: + +### With fcgiwrap + +```bash +# Install fcgiwrap +# Start it (systemd, OpenRC, or manual) +fcgiwrap -s unix:/run/fcgiwrap.sock & +``` + +```nginx +server { + listen 80; + server_name git.example.com; + + root /usr/share/cgit; + + # Serve static files directly + location /cgit-data/ { + alias /usr/share/cgit/; + } + + # Pass CGI requests to fcgiwrap + location /cgit { + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi; + fastcgi_param PATH_INFO $uri; + fastcgi_param QUERY_STRING $args; + fastcgi_param HTTP_HOST $server_name; + fastcgi_pass unix:/run/fcgiwrap.sock; + } +} +``` + +### With spawn-fcgi + +```bash +spawn-fcgi -s /run/cgit.sock -n -- /usr/bin/fcgiwrap +``` + +## lighttpd Configuration + +```lighttpd +server.modules += ("mod_cgi", "mod_alias", "mod_rewrite") + +alias.url = ( + "/cgit-data/" => "/usr/share/cgit/", + "/cgit/" => "/usr/lib/cgit/cgit.cgi" +) + +cgi.assign = ( + "cgit.cgi" => "" +) + +url.rewrite-once = ( + "^/cgit/(.*)$" => "/cgit/cgit.cgi/$1" +) +``` + +## Configuration File + +Create `/etc/cgitrc`: + +```ini +# Site identity +root-title=My Git Server +root-desc=Git repository browser +css=/cgit-data/cgit.css +logo=/cgit-data/cgit.png +favicon=/cgit-data/favicon.ico + +# URL routing +virtual-root=/cgit/ + +# Features +enable-commit-graph=1 +enable-blame=1 +enable-http-clone=1 +enable-index-links=1 +snapshots=tar.gz tar.xz zip +max-stats=quarter + +# Caching (recommended for production) +cache-size=1000 +cache-root=/var/cache/cgit +cache-root-ttl=5 +cache-repo-ttl=5 +cache-static-ttl=-1 + +# Repository discovery +scan-path=/srv/git/ +section-from-path=1 +enable-git-config=1 + +# Filters +source-filter=exec:/usr/lib/cgit/filters/syntax-highlighting.py +about-filter=exec:/usr/lib/cgit/filters/about-formatting.sh +``` + +## Cache Directory Setup + +```bash +# Create cache directory +mkdir -p /var/cache/cgit + +# Set ownership to web server user +chown www-data:www-data /var/cache/cgit +chmod 700 /var/cache/cgit + +# Optional: periodic cleanup cron job +echo "*/30 * * * * find /var/cache/cgit -type f -mmin +60 -delete" | \ + crontab -u www-data - +``` + +## Repository Permissions + +The web server user needs read access to all git repositories: + +```bash +# Option 1: Add web server user to git group +usermod -aG git www-data + +# Option 2: Set directory permissions +chmod -R g+rX /srv/git/ + +# Option 3: Use ACLs +setfacl -R -m u:www-data:rX /srv/git/ +setfacl -R -d -m u:www-data:rX /srv/git/ +``` + +## HTTPS Setup + +For production, serve cgit over HTTPS: + +```nginx +server { + listen 443 ssl; + server_name git.example.com; + + ssl_certificate /etc/ssl/certs/git.example.com.pem; + ssl_certificate_key /etc/ssl/private/git.example.com.key; + + # ... cgit configuration ... +} + +server { + listen 80; + server_name git.example.com; + return 301 https://$server_name$request_uri; +} +``` + +## Performance Tuning + +### Enable Caching + +The response cache is essential for performance: + +```ini +cache-size=1000 # number of cache entries +cache-root-ttl=5 # repo list: 5 minutes +cache-repo-ttl=5 # repo pages: 5 minutes +cache-static-ttl=-1 # static content: forever +cache-about-ttl=15 # about pages: 15 minutes +``` + +### Limit Resource Usage + +```ini +max-repo-count=100 # repos per page +max-commit-count=50 # commits per page +max-blob-size=512 # max blob display (KB) +max-message-length=120 # truncate long subjects +max-repodesc-length=80 # truncate descriptions +``` + +### Use Lua Filters + +Lua filters avoid fork/exec overhead: + +```ini +source-filter=lua:/usr/share/cgit/filters/syntax-highlight.lua +email-filter=lua:/usr/share/cgit/filters/email-libravatar.lua +``` + +### Optimize Git Access + +```bash +# Run periodic git gc on repositories +for repo in /srv/git/*.git; do + git -C "$repo" gc --auto +done + +# Ensure pack files are optimized +for repo in /srv/git/*.git; do + git -C "$repo" repack -a -d +done +``` + +## Monitoring + +### Check Cache Status + +```bash +# Count cache entries +ls /var/cache/cgit/ | wc -l + +# Check cache hit rate (if access logs are enabled) +grep "cgit.cgi" /var/log/nginx/access.log | tail -100 +``` + +### Health Check + +```bash +# Verify cgit is responding +curl -s -o /dev/null -w "%{http_code}" http://localhost/cgit/ +``` + +## Docker Deployment + +```dockerfile +FROM alpine:latest + +RUN apk add --no-cache \ + git make gcc musl-dev openssl-dev zlib-dev lua5.3-dev \ + fcgiwrap nginx + +COPY cgit/ /build/cgit/ +WORKDIR /build/cgit +RUN make && make install + +COPY cgitrc /etc/cgitrc +COPY nginx.conf /etc/nginx/conf.d/cgit.conf + +EXPOSE 80 +CMD ["sh", "-c", "fcgiwrap -s unix:/run/fcgiwrap.sock & nginx -g 'daemon off;'"] +``` + +## systemd Service + +```ini +# /etc/systemd/system/fcgiwrap-cgit.service +[Unit] +Description=fcgiwrap for cgit +After=network.target + +[Service] +ExecStart=/usr/bin/fcgiwrap -s unix:/run/fcgiwrap.sock +User=www-data +Group=www-data + +[Install] +WantedBy=multi-user.target +``` + +## Troubleshooting + +| Symptom | Cause | Solution | +|---------|-------|----------| +| 500 Internal Server Error | CGI binary not executable | `chmod +x cgit.cgi` | +| Blank page | Missing CSS path | Check `css=` directive | +| No repositories shown | Wrong `scan-path` | Verify path and permissions | +| Cache errors | Permission denied | Fix cache dir ownership | +| Lua filter fails | Lua not compiled in | Rebuild without `NO_LUA` | +| Clone fails | `enable-http-clone=0` | Set to `1` | +| Missing styles | Static file alias wrong | Check web server alias config | +| Timeout on large repos | No caching | Enable `cache-size` | diff --git a/docs/handbook/cgit/diff-engine.md b/docs/handbook/cgit/diff-engine.md new file mode 100644 index 0000000000..c82092842c --- /dev/null +++ b/docs/handbook/cgit/diff-engine.md @@ -0,0 +1,352 @@ +# cgit — Diff Engine + +## Overview + +cgit's diff engine renders differences between commits, trees, and blobs. +It supports three diff modes: unified, side-by-side, and stat-only. The +engine leverages libgit's internal diff machinery and adds HTML rendering on +top. + +Source files: `ui-diff.c`, `ui-diff.h`, `ui-ssdiff.c`, `ui-ssdiff.h`, +`shared.c` (diff helpers). + +## Diff Types + +```c +#define DIFF_UNIFIED 0 /* traditional unified diff */ +#define DIFF_SSDIFF 1 /* side-by-side diff */ +#define DIFF_STATONLY 2 /* only show diffstat */ +``` + +The diff type is selected by the `ss` query parameter or the +`side-by-side-diffs` configuration directive. + +## Diffstat + +### File Info Structure + +```c +struct fileinfo { + char status; /* 'A'dd, 'D'elete, 'M'odify, 'R'ename, etc. */ + unsigned long old_size; + unsigned long new_size; + int binary; + struct object_id old_oid; /* old blob SHA */ + struct object_id new_oid; /* new blob SHA */ + unsigned short old_mode; + unsigned short new_mode; + char *old_path; + char *new_path; + int added; /* lines added */ + int removed; /* lines removed */ +}; +``` + +### Collecting File Changes: `inspect_filepair()` + +For each changed file in a commit, `inspect_filepair()` records the change +information: + +```c +static void inspect_filepair(struct diff_filepair *pair) +{ + /* populate a fileinfo entry from the diff_filepair */ + files++; + switch (pair->status) { + case DIFF_STATUS_ADDED: + info->status = 'A'; + break; + case DIFF_STATUS_DELETED: + info->status = 'D'; + break; + case DIFF_STATUS_MODIFIED: + info->status = 'M'; + break; + case DIFF_STATUS_RENAMED: + info->status = 'R'; + /* old_path and new_path differ */ + break; + case DIFF_STATUS_COPIED: + info->status = 'C'; + break; + /* ... */ + } +} +``` + +### Rendering Diffstat: `cgit_print_diffstat()` + +```c +void cgit_print_diffstat(const struct object_id *old, + const struct object_id *new, + const char *prefix) +``` + +Renders an HTML table showing changed files with bar graphs: + +```html +<table summary='diffstat' class='diffstat'> + <tr> + <td class='mode'>M</td> + <td class='upd'><a href='...'>src/main.c</a></td> + <td class='right'>42</td> + <td class='graph'> + <span class='add' style='width: 70%'></span> + <span class='rem' style='width: 30%'></span> + </td> + </tr> + ... + <tr class='total'> + <td colspan='3'>5 files changed, 120 insertions, 45 deletions</td> + </tr> +</table> +``` + +The bar graph width is calculated proportionally to the maximum changed +lines across all files. + +## Unified Diff + +### `cgit_print_diff()` + +The main diff rendering function: + +```c +void cgit_print_diff(const char *new_rev, const char *old_rev, + const char *prefix, int show_ctrls, int raw) +``` + +Parameters: +- `new_rev` — New commit SHA +- `old_rev` — Old commit SHA (optional; defaults to parent) +- `prefix` — Path prefix filter (show only diffs under this path) +- `show_ctrls` — Show diff controls (diff type toggle buttons) +- `raw` — Output raw diff without HTML wrapping + +### Diff Controls + +When `show_ctrls=1`, diff mode toggle buttons are rendered: + +```html +<div class='cgit-panel'> + <b>Diff options</b> + <form method='get' action='...'> + <select name='dt'> + <option value='0'>unified</option> + <option value='1'>ssdiff</option> + <option value='2'>stat only</option> + </select> + <input type='submit' value='Go'/> + </form> +</div> +``` + +### Filepair Callback: `filepair_cb()` + +For each changed file, `filepair_cb()` renders the diff: + +```c +static void filepair_cb(struct diff_filepair *pair) +{ + /* emit file header */ + htmlf("<div class='head'>%s</div>", pair->one->path); + /* set up diff options */ + xdiff_opts.ctxlen = ctx.qry.context ?: 3; + /* run the diff and emit line-by-line output */ + /* each line gets a CSS class: .add, .del, or .ctx */ +} +``` + +### Hunk Headers + +```c +void cgit_print_diff_hunk_header(int oldofs, int oldcnt, + int newofs, int newcnt, + const char *func) +``` + +Renders hunk headers as: + +```html +<div class='hunk'>@@ -oldofs,oldcnt +newofs,newcnt @@ func</div> +``` + +### Line Rendering + +Each diff line is rendered with a status prefix and CSS class: + +| Line Type | CSS Class | Prefix | +|-----------|----------|--------| +| Added | `.add` | `+` | +| Removed | `.del` | `-` | +| Context | `.ctx` | ` ` | +| Hunk header | `.hunk` | `@@` | + +## Side-by-Side Diff (`ui-ssdiff.c`) + +The side-by-side diff view renders old and new versions in adjacent columns. + +### LCS Algorithm + +`ui-ssdiff.c` implements a Longest Common Subsequence (LCS) algorithm to +align lines between old and new versions: + +```c +/* LCS computation for line alignment */ +static int *lcs(char *a, int an, char *b, int bn) +{ + int *prev, *curr; + /* dynamic programming: build LCS table */ + prev = calloc(bn + 1, sizeof(int)); + curr = calloc(bn + 1, sizeof(int)); + for (int i = 1; i <= an; i++) { + for (int j = 1; j <= bn; j++) { + if (a[i-1] == b[j-1]) + curr[j] = prev[j-1] + 1; + else + curr[j] = MAX(prev[j], curr[j-1]); + } + SWAP(prev, curr); + } + return prev; +} +``` + +### Deferred Lines + +Side-by-side rendering uses a deferred output model: + +```c +struct deferred_lines { + int line_no; + char *line; + struct deferred_lines *next; +}; +``` + +Lines are collected and paired before output. For modified lines, the LCS +algorithm identifies character-level changes and highlights them with +`<span class='add'>` or `<span class='del'>` within each line. + +### Tab Expansion + +```c +static char *replace_tabs(char *line) +``` + +Tabs are expanded to spaces for proper column alignment in side-by-side +view. The tab width is 8 characters. + +### Rendering + +Side-by-side output uses a two-column `<table>`: + +```html +<table class='ssdiff'> + <tr> + <td class='lineno'><a>42</a></td> + <td class='del'>old line content</td> + <td class='lineno'><a>42</a></td> + <td class='add'>new line content</td> + </tr> +</table> +``` + +Changed characters within a line are highlighted with inline spans. + +## Low-Level Diff Helpers (`shared.c`) + +### Tree Diff + +```c +void cgit_diff_tree(const struct object_id *old_oid, + const struct object_id *new_oid, + filepair_fn fn, const char *prefix, + int renamelimit) +``` + +Computes the diff between two tree objects (typically from two commits). +Calls `fn` for each changed file pair. `renamelimit` controls rename +detection threshold. + +### Commit Diff + +```c +void cgit_diff_commit(struct commit *commit, filepair_fn fn, + const char *prefix) +``` + +Diffs a commit against its first parent. For root commits (no parent), +diffs against an empty tree. + +### File Diff + +```c +void cgit_diff_files(const struct object_id *old_oid, + const struct object_id *new_oid, + unsigned long *old_size, + unsigned long *new_size, + int *binary, int context, + int ignorews, linediff_fn fn) +``` + +Performs a line-level diff between two blobs. The `linediff_fn` callback is +invoked for each output line (add/remove/context). + +## Diff in Context: Commit View + +`ui-commit.c` uses the diff engine to show changes in commit view: + +```c +void cgit_print_commit(const char *rev, const char *prefix) +{ + /* ... commit metadata ... */ + cgit_print_diff(ctx.qry.sha1, info->parent_sha1, prefix, 0, 0); +} +``` + +## Diff in Context: Log View + +`ui-log.c` can optionally show per-commit diffstats: + +```c +if (ctx.cfg.enable_log_filecount) { + cgit_diff_commit(commit, inspect_filepair, NULL); + /* display changed files count, added/removed */ +} +``` + +## Binary Detection + +Files are marked as binary when diffing if the content contains null bytes +or exceeds the configured max-blob-size. Binary files are shown as: + +``` +Binary files differ +``` + +No line-level diff is performed for binary content. + +## Diff Configuration + +| Directive | Default | Effect | +|-----------|---------|--------| +| `side-by-side-diffs` | 0 | Default diff type | +| `renamelimit` | -1 | Rename detection limit | +| `max-blob-size` | 0 | Max blob size for display | +| `enable-log-filecount` | 0 | Show file counts in log | +| `enable-log-linecount` | 0 | Show line counts in log | + +## Raw Diff Output + +The `rawdiff` command outputs a plain-text unified diff without HTML +wrapping, suitable for piping or downloading: + +```c +static void cmd_rawdiff(struct cgit_context *ctx) +{ + ctx->page.mimetype = "text/plain"; + cgit_print_diff(ctx->qry.sha1, ctx->qry.sha2, + ctx->qry.path, 0, 1 /* raw */); +} +``` diff --git a/docs/handbook/cgit/filter-system.md b/docs/handbook/cgit/filter-system.md new file mode 100644 index 0000000000..be6f94e4b7 --- /dev/null +++ b/docs/handbook/cgit/filter-system.md @@ -0,0 +1,358 @@ +# cgit — Filter System + +## Overview + +cgit provides a pluggable content filtering pipeline that transforms text +before it is rendered in HTML output. Filters are used for tasks such as +syntax highlighting, README rendering, email obfuscation, and authentication. + +Source file: `filter.c`. + +## Filter Types + +Six filter types are defined, each identified by a constant and linked to an +entry in the `filter_specs[]` table: + +```c +#define ABOUT_FILTER 0 /* README/about page rendering */ +#define COMMIT_FILTER 1 /* commit message formatting */ +#define SOURCE_FILTER 2 /* source code syntax highlighting */ +#define EMAIL_FILTER 3 /* email address display */ +#define AUTH_FILTER 4 /* authentication/authorization */ +#define OWNER_FILTER 5 /* owner field display */ +``` + +### Filter Specs Table + +```c +static struct { + char *prefix; + int args; +} filter_specs[] = { + [ABOUT_FILTER] = { "about", 1 }, + [COMMIT_FILTER] = { "commit", 0 }, + [SOURCE_FILTER] = { "source", 1 }, + [EMAIL_FILTER] = { "email", 2 }, /* email, page */ + [AUTH_FILTER] = { "auth", 12 }, + [OWNER_FILTER] = { "owner", 0 }, +}; +``` + +The `args` field specifies the number of *extra* arguments the filter +receives (beyond the filter command itself). + +## Filter Structure + +```c +struct cgit_filter { + char *cmd; /* command or script path */ + int type; /* filter type constant */ + int (*open)(struct cgit_filter *, ...); /* start filter */ + int (*close)(struct cgit_filter *); /* finish filter */ + void (*fprintf)(struct cgit_filter *, FILE *, const char *fmt, ...); + void (*cleanup)(struct cgit_filter *); /* free resources */ + int argument_count; /* from filter_specs */ +}; +``` + +Two implementations exist: + +| Implementation | Struct | Description | +|---------------|--------|-------------| +| Exec filter | `struct cgit_exec_filter` | Fork/exec an external process | +| Lua filter | `struct cgit_lua_filter` | Execute a Lua script in-process | + +## Exec Filters + +Exec filters fork a child process and redirect `stdout` through a pipe. All +data written to `stdout` while the filter is open passes through the child +process, which can transform it before output. + +### Structure + +```c +struct cgit_exec_filter { + struct cgit_filter base; + char *cmd; + char **argv; + int old_stdout; /* saved fd for restoring stdout */ + int pipe_fh[2]; /* pipe: [read, write] */ + pid_t pid; /* child process id */ +}; +``` + +### Open Phase + +```c +static int open_exec_filter(struct cgit_filter *base, ...) +{ + struct cgit_exec_filter *f = (struct cgit_exec_filter *)base; + /* create pipe */ + pipe(f->pipe_fh); + /* save stdout */ + f->old_stdout = dup(STDOUT_FILENO); + /* fork */ + f->pid = fork(); + if (f->pid == 0) { + /* child: redirect stdin from pipe read end */ + dup2(f->pipe_fh[0], STDIN_FILENO); + close(f->pipe_fh[0]); + close(f->pipe_fh[1]); + /* exec the filter command with extra args from va_list */ + execvp(f->cmd, f->argv); + /* on failure: */ + exit(1); + } + /* parent: redirect stdout to pipe write end */ + dup2(f->pipe_fh[1], STDOUT_FILENO); + close(f->pipe_fh[0]); + close(f->pipe_fh[1]); + return 0; +} +``` + +### Close Phase + +```c +static int close_exec_filter(struct cgit_filter *base) +{ + struct cgit_exec_filter *f = (struct cgit_exec_filter *)base; + int status; + fflush(stdout); + /* restore original stdout */ + dup2(f->old_stdout, STDOUT_FILENO); + close(f->old_stdout); + /* wait for child */ + waitpid(f->pid, &status, 0); + /* return child exit status */ + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return -1; +} +``` + +### Argument Passing + +Extra arguments (from `filter_specs[].args`) are passed via `va_list` in the +open function and become `argv` entries for the child process: + +| Filter Type | argv[0] | argv[1] | argv[2] | ... | +|-------------|---------|---------|---------|-----| +| ABOUT | cmd | filename | — | — | +| SOURCE | cmd | filename | — | — | +| COMMIT | cmd | — | — | — | +| OWNER | cmd | — | — | — | +| EMAIL | cmd | email | page | — | +| AUTH | cmd | (12 args: method, mimetype, http_host, https, authenticated, username, http_cookie, request_method, query_string, referer, path, http_accept) | + +## Lua Filters + +When cgit is compiled with Lua support, filters can be Lua scripts executed +in-process without fork/exec overhead. + +### Structure + +```c +struct cgit_lua_filter { + struct cgit_filter base; + char *script_file; + lua_State *lua_state; +}; +``` + +### Lua API + +The Lua script must define a `filter_open()` and `filter_close()` function. +Data is passed to the Lua script through a custom `write()` function +registered in the Lua environment. + +```lua +-- Example source filter +function filter_open(filename) + -- Called when the filter opens + -- filename is the file being processed +end + +function write(str) + -- Called with chunks of content to filter + -- Write transformed output + html(str) +end + +function filter_close() + -- Called when filtering is complete + return 0 -- return exit code +end +``` + +### Lua C Bindings + +cgit registers several C functions into the Lua environment: + +```c +lua_pushcfunction(lua_state, lua_html); /* html() */ +lua_pushcfunction(lua_state, lua_html_txt); /* html_txt() */ +lua_pushcfunction(lua_state, lua_html_attr); /* html_attr() */ +lua_pushcfunction(lua_state, lua_html_url_path); /* html_url_path() */ +lua_pushcfunction(lua_state, lua_html_url_arg); /* html_url_arg() */ +lua_pushcfunction(lua_state, lua_html_include); /* include() */ +``` + +These correspond to the C functions in `html.c` and allow the Lua script to +produce properly escaped HTML output. + +### Lua Filter Open + +```c +static int open_lua_filter(struct cgit_filter *base, ...) +{ + struct cgit_lua_filter *f = (struct cgit_lua_filter *)base; + /* Load and execute the Lua script if not already loaded */ + if (!f->lua_state) { + f->lua_state = luaL_newstate(); + luaL_openlibs(f->lua_state); + /* register C bindings */ + /* load script file */ + } + /* redirect write() calls to the Lua state */ + /* call filter_open() in the Lua script, passing extra args */ + return 0; +} +``` + +### Lua Filter Close + +```c +static int close_lua_filter(struct cgit_filter *base) +{ + struct cgit_lua_filter *f = (struct cgit_lua_filter *)base; + /* call filter_close() in the Lua script */ + /* return the script's exit code */ + return lua_tointeger(f->lua_state, -1); +} +``` + +## Filter Construction + +`cgit_new_filter()` creates a new filter instance: + +```c +struct cgit_filter *cgit_new_filter(const char *cmd, filter_type type) +{ + if (!cmd || !*cmd) + return NULL; + + if (!prefixcmp(cmd, "lua:")) { + /* create Lua filter */ + return new_lua_filter(cmd + 4, type); + } + if (!prefixcmp(cmd, "exec:")) { + /* create exec filter, stripping prefix */ + return new_exec_filter(cmd + 5, type); + } + /* default: treat as exec filter */ + return new_exec_filter(cmd, type); +} +``` + +Prefix rules: +- `lua:/path/to/script.lua` → Lua filter +- `exec:/path/to/script` → exec filter +- `/path/to/script` (no prefix) → exec filter (backward compatibility) + +## Filter Usage Points + +### About Filter (`ABOUT_FILTER`) + +Applied when rendering README and about pages. Called from `ui-summary.c` +and the about view: + +```c +cgit_open_filter(ctx.repo->about_filter, filename); +/* write README content */ +cgit_close_filter(ctx.repo->about_filter); +``` + +Common use: converting Markdown to HTML. + +### Source Filter (`SOURCE_FILTER`) + +Applied when displaying file contents in blob/tree views. Called from +`ui-tree.c`: + +```c +cgit_open_filter(ctx.repo->source_filter, filename); +/* write file content */ +cgit_close_filter(ctx.repo->source_filter); +``` + +Common use: syntax highlighting. + +### Commit Filter (`COMMIT_FILTER`) + +Applied to commit messages in log and commit views. Called from `ui-log.c` +and `ui-commit.c`: + +```c +cgit_open_filter(ctx.repo->commit_filter); +html_txt(info->msg); +cgit_close_filter(ctx.repo->commit_filter); +``` + +Common use: linkifying issue references. + +### Email Filter (`EMAIL_FILTER`) + +Applied to author/committer email addresses. Receives the email address and +current page name as arguments: + +```c +cgit_open_filter(ctx.repo->email_filter, email, page); +html_txt(email); +cgit_close_filter(ctx.repo->email_filter); +``` + +Common use: gravatar integration, email obfuscation. + +### Auth Filter (`AUTH_FILTER`) + +Used for cookie-based authentication. Receives 12 arguments covering the +full HTTP request context. See `authentication.md` for details. + +### Owner Filter (`OWNER_FILTER`) + +Applied when displaying the repository owner. + +## Shipped Filter Scripts + +cgit ships with filter scripts in the `filters/` directory: + +| Script | Type | Description | +|--------|------|-------------| +| `syntax-highlighting.py` | SOURCE | Python-based syntax highlighter using Pygments | +| `syntax-highlighting.sh` | SOURCE | Shell-based highlighter (highlight command) | +| `about-formatting.sh` | ABOUT | Renders markdown via `markdown` or `rst2html` | +| `html-converters/md2html` | ABOUT | Standalone markdown-to-HTML converter | +| `html-converters/rst2html` | ABOUT | reStructuredText-to-HTML converter | +| `html-converters/txt2html` | ABOUT | Plain text to HTML converter | +| `email-gravatar.py` | EMAIL | Adds gravatar avatars | +| `email-libravatar.lua` | EMAIL | Lua-based libravatar integration | +| `simple-hierarchical-auth.lua` | AUTH | Lua path-based authentication | + +## Error Handling + +If an exec filter's child process exits with a non-zero status, `close()` +returns that status code. The calling code can check this to fall back to +unfiltered output. + +If a Lua filter throws an error, the error message is logged via +`die("lua error")` and the filter is aborted. + +## Performance Considerations + +- **Exec filters** have per-invocation fork/exec overhead. For high-traffic + sites, consider Lua filters or enabling the response cache. +- **Lua filters** run in-process with no fork overhead but require Lua support + to be compiled in. +- Filters are not called when serving cached responses — the cached output + already includes the filtered content. diff --git a/docs/handbook/cgit/html-rendering.md b/docs/handbook/cgit/html-rendering.md new file mode 100644 index 0000000000..dab14d66b2 --- /dev/null +++ b/docs/handbook/cgit/html-rendering.md @@ -0,0 +1,380 @@ +# cgit — HTML Rendering Engine + +## Overview + +cgit generates all HTML output through a set of low-level rendering functions +defined in `html.c` and `html.h`. These functions handle entity escaping, +URL encoding, and formatted output. Higher-level page structure is built by +`ui-shared.c`. + +Source files: `html.c`, `html.h`, `ui-shared.c`, `ui-shared.h`. + +## Output Model + +All output functions write directly to `stdout` via `write(2)`. There is no +internal buffering beyond the standard I/O buffer. This design works because +cgit runs as a CGI process — each request is a separate process with its own +stdout connected to the web server. + +## Core Output Functions + +### Raw Output + +```c +void html_raw(const char *data, size_t size); +``` + +Writes raw bytes to stdout without any escaping. Used for binary content +and pre-escaped strings. + +### Escaped Text Output + +```c +void html(const char *txt); +``` + +Writes a string with HTML entity escaping: +- `<` → `<` +- `>` → `>` +- `&` → `&` + +```c +void html_txt(const char *txt); +``` + +Same as `html()` but also escapes: +- `"` → `"` +- `'` → `'` + +Used for text content that appears inside HTML tags. + +```c +void html_ntxt(const char *txt, int len); +``` + +Length-limited version of `html_txt()`. Writes at most `len` characters, +appending `...` if truncated. + +### Attribute Escaping + +```c +void html_attr(const char *txt); +``` + +Escapes text for use in HTML attribute values. Escapes the same characters +as `html_txt()`. + +## URL Encoding + +### URL Escape Table + +`html.c` defines a 256-entry escape table for URL encoding: + +```c +static const char *url_escape_table[256] = { + "%00", "%01", "%02", ..., + [' '] = "+", + ['!'] = NULL, /* pass through */ + ['"'] = "%22", + ['#'] = "%23", + ['%'] = "%25", + ['&'] = "%26", + ['+'] = "%2B", + ['?'] = "%3F", + /* letters, digits, '-', '_', '.', '~' pass through (NULL) */ + ... +}; +``` + +Characters with a `NULL` entry pass through unmodified. All others are +replaced with their percent-encoded representations. + +### URL Path Encoding + +```c +void html_url_path(const char *txt); +``` + +Encodes a URL path component. Uses `url_escape_table` but preserves `/` +characters (they are structural in paths). + +### URL Argument Encoding + +```c +void html_url_arg(const char *txt); +``` + +Encodes a URL query parameter value. Uses `url_escape_table` including +encoding `/` characters. + +## Formatted Output + +### `fmt()` — Ring Buffer Formatter + +```c +const char *fmt(const char *format, ...); +``` + +A `printf`-style formatter that returns a pointer to an internal static +buffer. Uses a ring of 8 buffers (each 8 KB) to allow multiple `fmt()` +calls in a single expression: + +```c +#define FMT_BUFS 8 +#define FMT_SIZE 8192 + +static char bufs[FMT_BUFS][FMT_SIZE]; +static int bufidx; + +const char *fmt(const char *format, ...) +{ + bufidx = (bufidx + 1) % FMT_BUFS; + va_list args; + va_start(args, format); + vsnprintf(bufs[bufidx], FMT_SIZE, format, args); + va_end(args); + return bufs[bufidx]; +} +``` + +This is used extensively throughout cgit for constructing strings without +explicit memory management. The ring buffer avoids use-after-free for up to +8 nested calls. + +### `fmtalloc()` — Heap Formatter + +```c +char *fmtalloc(const char *format, ...); +``` + +Like `fmt()` but allocates a new heap buffer with `xstrfmt()`. Used when +the result must outlive the ring buffer cycle. + +### `htmlf()` — Formatted HTML + +```c +void htmlf(const char *format, ...); +``` + +`printf`-style output directly to stdout. Does NOT perform HTML escaping — +the caller must ensure the format string and arguments are safe. + +## Form Helpers + +### Hidden Fields + +```c +void html_hidden(const char *name, const char *value); +``` + +Generates a hidden form field: + +```html +<input type='hidden' name='name' value='value' /> +``` + +Values are attribute-escaped. + +### Option Elements + +```c +void html_option(const char *value, const char *text, const char *selected_value); +``` + +Generates an `<option>` element, marking it as selected if `value` matches +`selected_value`: + +```html +<option value='value' selected='selected'>text</option> +``` + +### Checkbox Input + +```c +void html_checkbox(const char *name, int value); +``` + +Generates a checkbox input. + +### Text Input + +```c +void html_txt_input(const char *name, const char *value, int size); +``` + +Generates a text input field. + +## Link Generation + +```c +void html_link_open(const char *url, const char *title, const char *class); +void html_link_close(void); +``` + +Generate `<a>` tags with optional title and class attributes. URL is +path-escaped. + +## File Inclusion + +```c +void html_include(const char *filename); +``` + +Reads a file from disk and writes its contents to stdout without escaping. +Used for header/footer file inclusion configured via the `header` and +`footer` directives. + +## Page Structure (`ui-shared.c`) + +### HTTP Headers + +```c +void cgit_print_http_headers(void); +``` + +Emits HTTP response headers based on `ctx.page`: + +``` +Status: 200 OK +Content-Type: text/html; charset=utf-8 +Last-Modified: Thu, 01 Jan 2024 00:00:00 GMT +Expires: Thu, 01 Jan 2024 01:00:00 GMT +ETag: "abc123" +``` + +Fields are only emitted when the corresponding `ctx.page` fields are set. + +### HTML Document Head + +```c +void cgit_print_docstart(void); +``` + +Emits the HTML5 doctype, `<html>`, and `<head>` section: + +```html +<!DOCTYPE html> +<html lang='en'> +<head> + <title>repo - page</title> + <meta name='generator' content='cgit v0.0.5-1-Project-Tick'/> + <meta name='robots' content='index, nofollow'/> + <link rel='stylesheet' href='/cgit/cgit.css'/> + <link rel='icon' href='/favicon.ico'/> +</head> +``` + +### Page Header + +```c +void cgit_print_pageheader(void); +``` + +Renders the page header with logo, navigation tabs, and search form. +Navigation tabs are context-sensitive — repository pages show +summary/refs/log/tree/commit/diff/stats/etc. + +### Page Footer + +```c +void cgit_print_docend(void); +``` + +Closes the HTML document with footer content and closing tags. + +### Full Page Layout + +```c +void cgit_print_layout_start(void); +void cgit_print_layout_end(void); +``` + +These wrap the page content, calling `cgit_print_http_headers()`, +`cgit_print_docstart()`, `cgit_print_pageheader()`, etc. Commands with +`want_layout=1` have their output wrapped in this skeleton. + +## Repository Navigation + +```c +void cgit_print_repoheader(void); +``` + +For each page within a repository, renders: +- Repository name and description +- Navigation tabs: summary, refs, log, tree, commit, diff, stats +- Clone URLs +- Badges + +## Link Functions + +`ui-shared.c` provides numerous helper functions for generating +context-aware links: + +```c +void cgit_summary_link(const char *name, const char *title, + const char *class, const char *head); +void cgit_tag_link(const char *name, const char *title, + const char *class, const char *tag); +void cgit_tree_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +void cgit_log_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path, + int ofs, const char *grep, const char *pattern, + int showmsg, int follow); +void cgit_commit_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +void cgit_patch_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +void cgit_refs_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +void cgit_diff_link(const char *name, const char *title, + const char *class, const char *head, + const char *new_rev, const char *old_rev, + const char *path, int toggle_hierarchical_threading); +void cgit_stats_link(const char *name, const char *title, + const char *class, const char *head, + const char *path); +void cgit_plain_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +void cgit_blame_link(const char *name, const char *title, + const char *class, const char *head, + const char *rev, const char *path); +void cgit_object_link(struct object *obj); +void cgit_submodule_link(const char *name, const char *path, + const char *commit); +``` + +Each function builds a complete `<a>` tag with the appropriate URL, including +all required query parameters for the target page. + +## Diff Output Helpers + +```c +void cgit_print_diff_hunk_header(int oldofs, int oldcnt, + int newofs, int newcnt, const char *func); +void cgit_print_diff_line_prefix(int type); +``` + +These render diff hunks with proper CSS classes for syntax coloring (`.add`, +`.del`, `.hunk`). + +## Error Pages + +```c +void cgit_print_error(const char *msg); +void cgit_print_error_page(int code, const char *msg, const char *fmt, ...); +``` + +`cgit_print_error_page()` sets the HTTP status code and wraps the error +message in a full page layout. + +## Encoding + +All text output assumes UTF-8. The `Content-Type` header is always +`charset=utf-8`. There is no character set conversion. diff --git a/docs/handbook/cgit/lua-integration.md b/docs/handbook/cgit/lua-integration.md new file mode 100644 index 0000000000..26d605862e --- /dev/null +++ b/docs/handbook/cgit/lua-integration.md @@ -0,0 +1,428 @@ +# 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`: + +```makefile +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: `luajit` → `lua` → `lua5.2` → `lua5.1`. + +To disable Lua even when available: + +```bash +make NO_LUA=1 +``` + +The `HAVE_LUA` preprocessor define gates all Lua-related code: + +```c +#ifdef HAVE_LUA +/* Lua filter implementation */ +#else +/* stub: cgit_new_filter() returns NULL for lua: prefix */ +#endif +``` + +## Lua Filter Structure + +```c +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): + +```c +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: + +```c +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: + +```c +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: + +```c +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: + +```c +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: + +```c +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: + +```c +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: + +```c +/* 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 + +```c +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 + +```c +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: + +```lua +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 + +```lua +-- 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 + +```lua +-- 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 + +```lua +-- 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) + +```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 + +```ini +# 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 +``` diff --git a/docs/handbook/cgit/overview.md b/docs/handbook/cgit/overview.md new file mode 100644 index 0000000000..bb09d33e8b --- /dev/null +++ b/docs/handbook/cgit/overview.md @@ -0,0 +1,262 @@ +# cgit — Overview + +## What Is cgit? + +cgit is a fast, lightweight web frontend for Git repositories, implemented as a +CGI application written in C. It links directly against libgit (the C library +that forms the core of the `git` command-line tool), giving it native access to +repository objects without spawning external processes for every request. This +design makes cgit one of the fastest Git web interfaces available. + +The Project Tick fork carries version `0.0.5-1-Project-Tick` (defined in the +top-level `Makefile` as `CGIT_VERSION`). It builds against Git 2.46.0 and +extends the upstream cgit with features such as subtree display, SPDX license +detection, badge support, Code of Conduct / CLA pages, root links, and an +enhanced summary page with repository metadata. + +## Key Design Goals + +| Goal | How cgit achieves it | +|------|---------------------| +| **Speed** | Direct libgit linkage; file-based response cache; `sendfile()` on Linux | +| **Security** | `GIT_CONFIG_NOSYSTEM=1` set at load time; HTML entity escaping in every output function; directory-traversal guards; auth-filter framework | +| **Simplicity** | Single CGI binary; flat config file (`cgitrc`); no database requirement | +| **Extensibility** | Pluggable filter system (exec / Lua) for about, commit, source, email, owner, and auth content | + +## Source File Map + +The entire cgit source tree lives in `cgit/`. Every `.c` file has a matching +`.h` (with a few exceptions such as `shared.c` and `parsing.c` which declare +their interfaces in `cgit.h`). + +### Core files + +| File | Purpose | +|------|---------| +| `cgit.h` | Master header — includes libgit headers; defines all major types (`cgit_repo`, `cgit_config`, `cgit_query`, `cgit_context`, etc.) and function prototypes | +| `cgit.c` | Entry point — `prepare_context()`, `config_cb()`, `querystring_cb()`, `process_request()`, `main()` | +| `shared.c` | Global variables (`cgit_repolist`, `ctx`); repo management (`cgit_add_repo`, `cgit_get_repoinfo`); diff helpers; parsing helpers | +| `parsing.c` | Commit/tag parsing (`cgit_parse_commit`, `cgit_parse_tag`, `cgit_parse_url`) | +| `cmd.c` | Command dispatch table — maps URL page names to handler functions | +| `cmd.h` | `struct cgit_cmd` definition; `cgit_get_cmd()` prototype | +| `configfile.c` | Generic `name=value` config parser (`parse_configfile`) | +| `configfile.h` | `configfile_value_fn` typedef; `parse_configfile` prototype | + +### Infrastructure files + +| File | Purpose | +|------|---------| +| `cache.c` / `cache.h` | File-based response cache — FNV-1 hashing, slot open/lock/fill/unlock cycle | +| `filter.c` | Filter framework — exec filters (fork/exec), Lua filters (`luaL_newstate`) | +| `html.c` / `html.h` | HTML output primitives — entity escaping, URL encoding, form helpers | +| `scan-tree.c` / `scan-tree.h` | Filesystem repository scanning — `scan_tree()`, `scan_projects()` | + +### UI modules (`ui-*.c` / `ui-*.h`) + +| Module | Page | Handler function | +|--------|------|-----------------| +| `ui-repolist` | `repolist` | `cgit_print_repolist()` | +| `ui-summary` | `summary` | `cgit_print_summary()` | +| `ui-log` | `log` | `cgit_print_log()` | +| `ui-commit` | `commit` | `cgit_print_commit()` | +| `ui-diff` | `diff` | `cgit_print_diff()` | +| `ui-tree` | `tree` | `cgit_print_tree()` | +| `ui-blob` | `blob` | `cgit_print_blob()` | +| `ui-refs` | `refs` | `cgit_print_refs()` | +| `ui-tag` | `tag` | `cgit_print_tag()` | +| `ui-snapshot` | `snapshot` | `cgit_print_snapshot()` | +| `ui-plain` | `plain` | `cgit_print_plain()` | +| `ui-blame` | `blame` | `cgit_print_blame()` | +| `ui-patch` | `patch` | `cgit_print_patch()` | +| `ui-atom` | `atom` | `cgit_print_atom()` | +| `ui-clone` | `HEAD` / `info` / `objects` | `cgit_clone_head()`, `cgit_clone_info()`, `cgit_clone_objects()` | +| `ui-stats` | `stats` | `cgit_show_stats()` | +| `ui-ssdiff` | (helper) | Side-by-side diff rendering via LCS algorithm | +| `ui-shared` | (helper) | HTTP headers, HTML page skeleton, link generation | + +### Static assets + +| File | Description | +|------|-------------| +| `cgit.css` | Default stylesheet | +| `cgit.js` | Client-side JavaScript (e.g. tree filtering) | +| `cgit.png` | Default logo | +| `favicon.ico` | Default favicon | +| `robots.txt` | Default robots file | + +## Core Data Structures + +All major types are defined in `cgit.h`. The single global +`struct cgit_context ctx` (declared in `shared.c`) holds the entire request +state: + +```c +struct cgit_context { + struct cgit_environment env; /* CGI environment variables */ + struct cgit_query qry; /* Parsed query/URL parameters */ + struct cgit_config cfg; /* Global configuration */ + struct cgit_repo *repo; /* Currently selected repository (or NULL) */ + struct cgit_page page; /* HTTP response metadata */ +}; +``` + +### `struct cgit_repo` + +Represents a single Git repository. Key fields: + +```c +struct cgit_repo { + char *url; /* URL-visible name (e.g. "myproject") */ + char *name; /* Display name */ + char *basename; /* Last path component */ + char *path; /* Filesystem path to .git directory */ + char *desc; /* Description string */ + char *owner; /* Repository owner */ + char *defbranch; /* Default branch (NULL → guess from HEAD) */ + char *section; /* Section for grouped display */ + char *clone_url; /* Clone URL override */ + char *homepage; /* Project homepage URL */ + struct string_list readme; /* README file references */ + struct string_list badges; /* Badge image URLs */ + int snapshots; /* Bitmask of enabled snapshot formats */ + int enable_blame; /* Whether blame view is enabled */ + int enable_commit_graph;/* Whether commit graph is shown in log */ + int enable_subtree; /* Whether subtree detection is enabled */ + int max_stats; /* Stats period index (0=disabled) */ + int hide; /* 1 = hidden from listing */ + int ignore; /* 1 = completely ignored */ + struct cgit_filter *about_filter; /* Per-repo about filter */ + struct cgit_filter *source_filter; /* Per-repo source highlighting */ + struct cgit_filter *email_filter; /* Per-repo email filter */ + struct cgit_filter *commit_filter; /* Per-repo commit message filter */ + struct cgit_filter *owner_filter; /* Per-repo owner filter */ + /* ... */ +}; +``` + +### `struct cgit_query` + +Holds all parsed URL/query-string parameters: + +```c +struct cgit_query { + int has_symref, has_oid, has_difftype; + char *raw; /* Raw query string */ + char *repo; /* Repository URL */ + char *page; /* Page name (log, commit, diff, ...) */ + char *search; /* Search query (q=) */ + char *grep; /* Search type (qt=) */ + char *head; /* Branch/ref (h=) */ + char *oid, *oid2; /* Object IDs (id=, id2=) */ + char *path; /* Path within repository */ + char *name; /* Snapshot filename */ + int ofs; /* Pagination offset */ + int showmsg; /* Show full commit messages in log */ + diff_type difftype; /* DIFF_UNIFIED / DIFF_SSDIFF / DIFF_STATONLY */ + int context; /* Diff context lines */ + int ignorews; /* Ignore whitespace in diffs */ + int follow; /* Follow renames in log */ + char *vpath; /* Virtual path (set by cmd dispatch) */ + /* ... */ +}; +``` + +## Request Lifecycle + +1. **Environment setup** — The `constructor_environment()` function runs before + `main()` (via `__attribute__((constructor))`). It sets + `GIT_CONFIG_NOSYSTEM=1` and `GIT_ATTR_NOSYSTEM=1`, then unsets `HOME` and + `XDG_CONFIG_HOME` to prevent Git from reading user/system configurations. + +2. **Context initialization** — `prepare_context()` zeroes out `ctx` and sets + all configuration defaults (cache sizes, TTLs, feature flags, etc.). CGI + environment variables are read from `getenv()`. + +3. **Configuration parsing** — `parse_configfile()` reads the cgitrc file + (default `/etc/cgitrc`, overridable via `$CGIT_CONFIG`) and calls + `config_cb()` for each `name=value` pair. Repository definitions begin with + `repo.url=` and subsequent `repo.*` directives configure that repository. + +4. **Query parsing** — If running in CGI mode (no `$NO_HTTP`), + `http_parse_querystring()` breaks the query string into name/value pairs and + passes them to `querystring_cb()`. The `url=` parameter is further parsed by + `cgit_parse_url()` which splits it into repo, page, and path components. + +5. **Authentication** — `authenticate_cookie()` checks whether an `auth-filter` + is configured. If so, it invokes the filter with function + `"authenticate-cookie"` and sets `ctx.env.authenticated` from the filter's + exit code. POST requests to `/?p=login` route through + `authenticate_post()` instead. + +6. **Cache lookup** — If caching is enabled (`cache-size > 0`), a cache key is + constructed from the URL and passed to `cache_process()`. On a cache hit the + stored response is sent directly via `sendfile()`. On a miss, stdout is + redirected to a lock file and the request proceeds through normal processing. + +7. **Command dispatch** — `cgit_get_cmd()` looks up `ctx.qry.page` in the + static `cmds[]` table (defined in `cmd.c`). If the command requires a + repository (`want_repo == 1`), the repository is initialized via + `prepare_repo_env()` and `prepare_repo_cmd()`. + +8. **Page rendering** — The matched command's handler function is called. Each + handler uses `cgit_print_http_headers()`, `cgit_print_docstart()`, + `cgit_print_pageheader()`, and `cgit_print_docend()` (from `ui-shared.c`) + to frame their output inside a proper HTML document. + +9. **Cleanup** — `cgit_cleanup_filters()` reaps all filter resources (closing + Lua states, freeing argv arrays). + +## Version String + +The version is compiled into the binary via: + +```makefile +CGIT_VERSION = 0.0.5-1-Project-Tick +``` + +and exposed as the global: + +```c +const char *cgit_version = CGIT_VERSION; +``` + +This string appears in the HTML footer (rendered by `ui-shared.c`) and in patch +output trailers. + +## Relationship to Git + +cgit is built *inside* the Git source tree. The `Makefile` downloads +Git 2.46.0, extracts it as a `git/` subdirectory, then calls `make -C git -f +../cgit.mk` which includes Git's own `Makefile` to inherit all build variables, +object files, and linker flags. The resulting `cgit` binary is a statically +linked combination of cgit's own object files and libgit. + +## Time Constants + +`cgit.h` defines convenience macros used for relative date display: + +```c +#define TM_MIN 60 +#define TM_HOUR (TM_MIN * 60) +#define TM_DAY (TM_HOUR * 24) +#define TM_WEEK (TM_DAY * 7) +#define TM_YEAR (TM_DAY * 365) +#define TM_MONTH (TM_YEAR / 12.0) +``` + +These are used by `cgit_print_age()` in `ui-shared.c` to render "2 hours ago" +style timestamps. + +## Default Encoding + +```c +#define PAGE_ENCODING "UTF-8" +``` + +All commit messages are re-encoded to UTF-8 before display (see +`cgit_parse_commit()` in `parsing.c`). + +## License + +cgit is licensed under the GNU General Public License v2. The `COPYING` file +in the cgit directory contains the full text. diff --git a/docs/handbook/cgit/repository-discovery.md b/docs/handbook/cgit/repository-discovery.md new file mode 100644 index 0000000000..9b961e74cf --- /dev/null +++ b/docs/handbook/cgit/repository-discovery.md @@ -0,0 +1,355 @@ +# cgit — Repository Discovery + +## Overview + +cgit discovers repositories through two mechanisms: explicit `repo.url=` +entries in the configuration file, and automatic filesystem scanning via +`scan-path`. The scan-tree subsystem recursively searches directories for +git repositories and auto-configures them. + +Source files: `scan-tree.c`, `scan-tree.h`, `shared.c` (repository list management). + +## Manual Repository Configuration + +Repositories can be explicitly defined in the cgitrc file: + +```ini +repo.url=myproject +repo.path=/srv/git/myproject.git +repo.desc=My project description +repo.owner=Alice +``` + +Each `repo.url=` triggers `cgit_add_repo()` in `shared.c`, which creates a +new `cgit_repo` entry in the global repository list. + +### `cgit_add_repo()` + +```c +struct cgit_repo *cgit_add_repo(const char *url) +{ + struct cgit_repo *ret; + /* grow the repo array if needed */ + if (cgit_repolist.count >= cgit_repolist.length) { + /* realloc with doubled capacity */ + } + ret = &cgit_repolist.repos[cgit_repolist.count++]; + /* initialize with defaults from ctx.cfg */ + ret->url = xstrdup(url); + ret->name = ret->url; + ret->path = NULL; + ret->desc = cgit_default_repo_desc; + ret->owner = NULL; + ret->section = ctx.cfg.section; + ret->snapshots = ctx.cfg.snapshots; + /* ... inherit all global defaults ... */ + return ret; +} +``` + +## Repository Lookup + +```c +struct cgit_repo *cgit_get_repoinfo(const char *url) +{ + int i; + for (i = 0; i < cgit_repolist.count; i++) { + if (!strcmp(cgit_repolist.repos[i].url, url)) + return &cgit_repolist.repos[i]; + } + return NULL; +} +``` + +This is a linear scan — adequate for typical installations with dozens to +hundreds of repositories. + +## Filesystem Scanning: `scan-path` + +The `scan-path` configuration directive triggers automatic repository +discovery. When encountered in the config file, `scan_tree()` or +`scan_projects()` is called immediately. + +### `scan_tree()` + +```c +void scan_tree(const char *path, repo_config_fn fn) +``` + +Recursively scans `path` for git repositories: + +```c +static void scan_path(const char *base, const char *path, repo_config_fn fn) +{ + DIR *dir; + struct dirent *ent; + + dir = opendir(path); + if (!dir) return; + + while ((ent = readdir(dir)) != NULL) { + /* skip "." and ".." */ + /* skip hidden directories unless scan-hidden-path=1 */ + + if (is_git_dir(fullpath)) { + /* found a bare repository */ + add_repo(base, fullpath, fn); + } else if (is_git_dir(fullpath + "/.git")) { + /* found a non-bare repository */ + add_repo(base, fullpath + "/.git", fn); + } else { + /* recurse into subdirectory */ + scan_path(base, fullpath, fn); + } + } + closedir(dir); +} +``` + +### Git Directory Detection: `is_git_dir()` + +```c +static int is_git_dir(const char *path) +{ + struct stat st; + struct strbuf pathbuf = STRBUF_INIT; + + /* check for path/HEAD */ + strbuf_addf(&pathbuf, "%s/HEAD", path); + if (stat(pathbuf.buf, &st)) { + strbuf_release(&pathbuf); + return 0; + } + + /* check for path/objects */ + strbuf_reset(&pathbuf); + strbuf_addf(&pathbuf, "%s/objects", path); + if (stat(pathbuf.buf, &st) || !S_ISDIR(st.st_mode)) { + strbuf_release(&pathbuf); + return 0; + } + + /* check for path/refs */ + strbuf_reset(&pathbuf); + strbuf_addf(&pathbuf, "%s/refs", path); + if (stat(pathbuf.buf, &st) || !S_ISDIR(st.st_mode)) { + strbuf_release(&pathbuf); + return 0; + } + + strbuf_release(&pathbuf); + return 1; +} +``` + +A directory is considered a git repository if it contains `HEAD`, `objects/`, +and `refs/` subdirectories. + +### Repository Registration: `add_repo()` + +When a git directory is found, `add_repo()` creates a repository entry: + +```c +static void add_repo(const char *base, const char *path, repo_config_fn fn) +{ + /* derive URL from path relative to base */ + /* strip .git suffix if remove-suffix is set */ + struct cgit_repo *repo = cgit_add_repo(url); + repo->path = xstrdup(path); + + /* read gitweb config from the repo */ + if (ctx.cfg.enable_git_config) { + char *gitconfig = fmt("%s/config", path); + parse_configfile(gitconfig, gitconfig_config); + } + + /* read owner from filesystem */ + if (!repo->owner) { + /* stat the repo dir and lookup uid owner */ + struct stat st; + if (!stat(path, &st)) { + struct passwd *pw = getpwuid(st.st_uid); + if (pw) + repo->owner = xstrdup(pw->pw_name); + } + } + + /* read description from description file */ + if (!repo->desc) { + char *descfile = fmt("%s/description", path); + /* read first line */ + } +} +``` + +### Git Config Integration: `gitconfig_config()` + +When `enable-git-config=1`, each discovered repository's `.git/config` is +parsed for metadata: + +```c +static int gitconfig_config(const char *key, const char *value) +{ + if (!strcmp(key, "gitweb.owner")) + repo_config(repo, "owner", value); + else if (!strcmp(key, "gitweb.description")) + repo_config(repo, "desc", value); + else if (!strcmp(key, "gitweb.category")) + repo_config(repo, "section", value); + else if (!strcmp(key, "gitweb.homepage")) + repo_config(repo, "homepage", value); + else if (skip_prefix(key, "cgit.", &name)) + repo_config(repo, name, value); + return 0; +} +``` + +This is compatible with gitweb's configuration keys and also supports +cgit-specific `cgit.*` keys. + +## Project List Scanning: `scan_projects()` + +```c +void scan_projects(const char *path, const char *projectsfile, + repo_config_fn fn) +``` + +Instead of recursively scanning a directory, reads a text file listing +project paths (one per line). Each path is appended to the base path and +checked with `is_git_dir()`. + +This is useful for large installations where full recursive scanning is too +slow. + +```ini +project-list=/etc/cgit/projects.list +scan-path=/srv/git +``` + +The `projects.list` file contains relative paths: + +``` +myproject.git +team/frontend.git +team/backend.git +``` + +## Section Derivation + +When `section-from-path` is set, repository sections are automatically +derived from the directory structure: + +| Value | Behavior | +|-------|----------| +| `0` | No auto-sectioning | +| `1` | First path component becomes section | +| `2` | First two components become section | +| `-1` | Last component becomes section | + +Example with `section-from-path=1` and `scan-path=/srv/git`: + +``` +/srv/git/team/project.git → section="team" +/srv/git/personal/test.git → section="personal" +``` + +## Age File + +The modification time of a repository is determined by: + +1. The `agefile` (default: `info/web/last-modified`) — if this file exists + in the repository, its contents (a date string) or modification time is + used +2. Otherwise, the mtime of the loose `refs/` directory +3. As a fallback, the repository directory's own mtime + +```c +static time_t read_agefile(const char *path) +{ + FILE *f; + static char buf[64]; + + f = fopen(path, "r"); + if (!f) + return -1; + if (fgets(buf, sizeof(buf), f)) { + fclose(f); + return parse_date(buf, NULL); + } + fclose(f); + /* fallback to file mtime */ + struct stat st; + if (!stat(path, &st)) + return st.st_mtime; + return 0; +} +``` + +## Repository List Management + +The global repository list is a dynamically-sized array: + +```c +struct cgit_repolist { + int count; + int length; /* allocated capacity */ + struct cgit_repo *repos; +}; + +struct cgit_repolist cgit_repolist; +``` + +### Sorting + +The repository list can be sorted by different criteria: + +```c +static int cmp_name(const void *a, const void *b); /* by name */ +static int cmp_section(const void *a, const void *b); /* by section */ +static int cmp_idle(const void *a, const void *b); /* by age */ +``` + +Sorting is controlled by the `repository-sort` directive and the `s` query +parameter. + +## Repository Visibility + +Two directives control repository visibility: + +| Directive | Effect | +|-----------|--------| +| `repo.hide=1` | Repository is hidden from the index but accessible by URL | +| `repo.ignore=1` | Repository is completely ignored | + +Additionally, `strict-export` restricts export to repositories containing a +specific file (e.g., `git-daemon-export-ok`): + +```ini +strict-export=git-daemon-export-ok +``` + +## Scan Path Caching + +Scanning large directory trees can be slow. The `cache-scanrc-ttl` directive +controls how long scan results are cached: + +```ini +cache-scanrc-ttl=15 # cache scan results for 15 minutes +``` + +When caching is enabled, the scan is performed only when the cached result +expires. + +## Configuration Reference + +| Directive | Default | Description | +|-----------|---------|-------------| +| `scan-path` | (none) | Directory to scan for repos | +| `project-list` | (none) | File listing project paths | +| `enable-git-config` | 0 | Read repo metadata from git config | +| `scan-hidden-path` | 0 | Include hidden directories in scan | +| `remove-suffix` | 0 | Strip `.git` suffix from URLs | +| `section-from-path` | 0 | Auto-derive section from path | +| `strict-export` | (none) | Required file for repo visibility | +| `agefile` | `info/web/last-modified` | File checked for repo age | +| `cache-scanrc-ttl` | 15 | TTL for cached scan results (minutes) | diff --git a/docs/handbook/cgit/snapshot-system.md b/docs/handbook/cgit/snapshot-system.md new file mode 100644 index 0000000000..bb39047f48 --- /dev/null +++ b/docs/handbook/cgit/snapshot-system.md @@ -0,0 +1,246 @@ +# cgit — Snapshot System + +## Overview + +cgit can generate downloadable source archives (snapshots) from any git +reference. Supported formats include tar, compressed tar variants, and zip. +The snapshot system validates requests against a configured format mask and +delegates archive generation to the git archive API. + +Source file: `ui-snapshot.c`, `ui-snapshot.h`. + +## Snapshot Format Table + +All supported formats are defined in `cgit_snapshot_formats[]`: + +```c +const struct cgit_snapshot_format cgit_snapshot_formats[] = { + { ".zip", "application/x-zip", write_zip_archive, 0x01 }, + { ".tar.gz", "application/x-gzip", write_tar_gzip_archive, 0x02 }, + { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive, 0x04 }, + { ".tar", "application/x-tar", write_tar_archive, 0x08 }, + { ".tar.xz", "application/x-xz", write_tar_xz_archive, 0x10 }, + { ".tar.zst", "application/x-zstd", write_tar_zstd_archive, 0x20 }, + { ".tar.lz", "application/x-lzip", write_tar_lzip_archive, 0x40 }, + { NULL } +}; +``` + +### Format Structure + +```c +struct cgit_snapshot_format { + const char *suffix; /* file extension */ + const char *mimetype; /* HTTP Content-Type */ + write_archive_fn_t fn; /* archive writer function */ + int bit; /* bitmask flag */ +}; +``` + +### Format Bitmask + +Each format has a power-of-two bit value. The `snapshots` configuration +directive sets a bitmask by OR-ing the bits of enabled formats: + +| Suffix | Bit | Hex | +|--------|-----|-----| +| `.zip` | 0x01 | 1 | +| `.tar.gz` | 0x02 | 2 | +| `.tar.bz2` | 0x04 | 4 | +| `.tar` | 0x08 | 8 | +| `.tar.xz` | 0x10 | 16 | +| `.tar.zst` | 0x20 | 32 | +| `.tar.lz` | 0x40 | 64 | +| all | 0x7F | 127 | + +### Parsing Snapshot Configuration + +`cgit_parse_snapshots_mask()` in `shared.c` converts the configuration +string to a bitmask: + +```c +int cgit_parse_snapshots_mask(const char *str) +{ + int mask = 0; + /* for each word in str */ + /* compare against cgit_snapshot_formats[].suffix */ + /* if match, mask |= format->bit */ + /* "all" enables all formats */ + return mask; +} +``` + +## Snapshot Request Processing + +### Entry Point: `cgit_print_snapshot()` + +```c +void cgit_print_snapshot(const char *head, const char *hex, + const char *prefix, const char *filename, + int snapshots) +``` + +Parameters: +- `head` — Branch/tag reference +- `hex` — Commit SHA +- `prefix` — Archive prefix (directory name within archive) +- `filename` — Requested filename (e.g., `myrepo-v1.0.tar.gz`) +- `snapshots` — Enabled format bitmask + +### Reference Resolution: `get_ref_from_filename()` + +Decomposes the requested filename into a reference and format: + +```c +static const struct cgit_snapshot_format *get_ref_from_filename( + const char *filename, char **ref) +{ + /* for each format suffix */ + /* if filename ends with suffix */ + /* extract the part before the suffix as the ref */ + /* return the matching format */ + /* strip repo prefix if present */ +} +``` + +Example decomposition: +- `myrepo-v1.0.tar.gz` → ref=`v1.0`, format=`.tar.gz` +- `myrepo-main.zip` → ref=`main`, format=`.zip` +- `myrepo-abc1234.tar.xz` → ref=`abc1234`, format=`.tar.xz` + +The prefix `myrepo-` is the `snapshot-prefix` (defaults to the repo basename). + +### Validation + +Before generating an archive, the function validates: + +1. **Format enabled**: The format's bit must be set in the snapshot mask +2. **Reference exists**: The ref must resolve to a valid git object +3. **Object type**: Must be a commit, tag, or tree + +### Archive Generation: `write_archive_type()` + +```c +static int write_archive_type(const char *format, const char *hex, + const char *prefix) +{ + struct archiver_args args; + memset(&args, 0, sizeof(args)); + args.base = prefix; /* directory prefix in archive */ + /* resolve hex to tree object */ + /* call write_archive() from libgit */ +} +``` + +The actual archive creation is delegated to Git's `write_archive()` API, +which handles tar and zip generation natively. + +### Compression Pipeline + +For compressed formats, the archive data is piped through compression: + +```c +static int write_tar_gzip_archive(/* ... */) +{ + /* pipe tar output through gzip compression */ +} + +static int write_tar_bzip2_archive(/* ... */) +{ + /* pipe tar output through bzip2 compression */ +} + +static int write_tar_xz_archive(/* ... */) +{ + /* pipe tar output through xz compression */ +} + +static int write_tar_zstd_archive(/* ... */) +{ + /* pipe tar output through zstd compression */ +} + +static int write_tar_lzip_archive(/* ... */) +{ + /* pipe tar output through lzip compression */ +} +``` + +## HTTP Response + +Snapshot responses include: + +``` +Content-Type: application/x-gzip +Content-Disposition: inline; filename="myrepo-v1.0.tar.gz" +``` + +The `Content-Disposition` header triggers a file download in browsers with +the correct filename. + +## Snapshot Links + +Snapshot links on repository pages are generated by `ui-shared.c`: + +```c +void cgit_print_snapshot_links(const char *repo, const char *head, + const char *hex, int snapshots) +{ + for (f = cgit_snapshot_formats; f->suffix; f++) { + if (!(snapshots & f->bit)) + continue; + /* generate link: repo/snapshot/prefix-ref.suffix */ + } +} +``` + +These links appear on the summary page and optionally in the log view. + +## Snapshot Prefix + +The archive prefix (directory name inside the archive) is determined by: + +1. `repo.snapshot-prefix` if set +2. Otherwise, the repository basename + +For a request like `myrepo-v1.0.tar.gz`, the archive contains files under +`myrepo-v1.0/`. + +## Signature Detection + +cgit can detect and display signature files alongside snapshots. When a +file matching `<snapshot-name>.asc` or `<snapshot-name>.sig` exists in the +repository, a signature link is shown next to the snapshot download. + +## Configuration + +| Directive | Default | Description | +|-----------|---------|-------------| +| `snapshots` | (none) | Space-separated list of enabled suffixes | +| `repo.snapshots` | (inherited) | Per-repo override | +| `repo.snapshot-prefix` | (basename) | Per-repo archive prefix | +| `cache-snapshot-ttl` | 5 min | Cache TTL for snapshot pages | + +### Enabling Snapshots + +```ini +# Global: enable tar.gz and zip for all repos +snapshots=tar.gz zip + +# Per-repo: enable all formats +repo.url=myrepo +repo.snapshots=all + +# Per-repo: disable snapshots +repo.url=internal-tools +repo.snapshots= +``` + +## Security Considerations + +- Snapshots are generated on-the-fly from git objects, so they always reflect + the repository's current state +- Large repositories can produce large archives — consider enabling caching + and setting appropriate `max-blob-size` limits +- Snapshot requests for non-existent refs return a 404 error page +- The snapshot filename is sanitized to prevent path traversal diff --git a/docs/handbook/cgit/testing.md b/docs/handbook/cgit/testing.md new file mode 100644 index 0000000000..ee7b5979f9 --- /dev/null +++ b/docs/handbook/cgit/testing.md @@ -0,0 +1,335 @@ +# cgit — Testing + +## Overview + +cgit has a shell-based test suite in the `tests/` directory. Tests use +Git's own test framework (`test-lib.sh`) and exercise cgit by invoking the +CGI binary with simulated HTTP requests. + +Source location: `cgit/tests/`. + +## Test Framework + +The test harness is built on Git's `test-lib.sh`, sourced from the vendored +Git tree at `git/t/test-lib.sh`. This provides: + +- TAP-compatible output +- Test assertions (`test_expect_success`, `test_expect_failure`) +- Temporary directory management (`trash` directories) +- Color-coded pass/fail reporting + +### `setup.sh` + +All test scripts source `tests/setup.sh`, which provides: + +```bash +# Core test helpers +prepare_tests() # Create repos and config file +run_test() # Execute a single test case +cgit_query() # Invoke cgit with a query string +cgit_url() # Invoke cgit with a virtual URL +strip_headers() # Remove HTTP headers from CGI output +``` + +### Invoking cgit + +Tests invoke cgit as a CGI binary by setting environment variables: + +```bash +cgit_query() +{ + CGIT_CONFIG="$PWD/cgitrc" QUERY_STRING="$1" cgit +} + +cgit_url() +{ + CGIT_CONFIG="$PWD/cgitrc" QUERY_STRING="url=$1" cgit +} +``` + +The `cgit` binary is on PATH (prepended by setup.sh). The response includes +HTTP headers followed by HTML content. `strip_headers()` removes the +headers for content-only assertions. + +## Test Repository Setup + +`setup_repos()` creates test repositories: + +```bash +setup_repos() +{ + rm -rf cache + mkdir -p cache + mkrepo repos/foo 5 # 5 commits + mkrepo repos/bar 50 commit-graph # 50 commits with commit-graph + mkrepo repos/foo+bar 10 testplus # 10 commits + special chars + mkrepo "repos/with space" 2 # repo with spaces in name + mkrepo repos/filter 5 testplus # for filter tests +} +``` + +### `mkrepo()` + +```bash +mkrepo() { + name=$1 + count=$2 + test_create_repo "$name" + ( + cd "$name" + n=1 + while test $n -le $count; do + echo $n >file-$n + git add file-$n + git commit -m "commit $n" + n=$(expr $n + 1) + done + case "$3" in + testplus) + echo "hello" >a+b + git add a+b + git commit -m "add a+b" + git branch "1+2" + ;; + commit-graph) + git commit-graph write + ;; + esac + ) +} +``` + +### Test Configuration + +A `cgitrc` file is generated in the test directory with: + +```ini +virtual-root=/ +cache-root=$PWD/cache +cache-size=1021 +snapshots=tar.gz tar.bz tar.lz tar.xz tar.zst zip +enable-log-filecount=1 +enable-log-linecount=1 +summary-log=5 +summary-branches=5 +summary-tags=5 +clone-url=git://example.org/$CGIT_REPO_URL.git +enable-filter-overrides=1 +root-coc=$PWD/site-coc.txt +root-cla=$PWD/site-cla.txt +root-homepage=https://projecttick.org +root-homepage-title=Project Tick +root-link=GitHub|https://github.com/example +root-link=GitLab|https://gitlab.com/example +root-link=Codeberg|https://codeberg.org/example + +repo.url=foo +repo.path=$PWD/repos/foo/.git + +repo.url=bar +repo.path=$PWD/repos/bar/.git +repo.desc=the bar repo + +repo.url=foo+bar +repo.path=$PWD/repos/foo+bar/.git +repo.desc=the foo+bar repo +# ... +``` + +## Test Scripts + +### Test File Naming + +Tests follow the convention `tNNNN-description.sh`: + +| Test | Description | +|------|-------------| +| `t0001-validate-git-versions.sh` | Verify Git version compatibility | +| `t0010-validate-html.sh` | Validate HTML output | +| `t0020-validate-cache.sh` | Test cache system | +| `t0101-index.sh` | Repository index page | +| `t0102-summary.sh` | Repository summary page | +| `t0103-log.sh` | Log view | +| `t0104-tree.sh` | Tree view | +| `t0105-commit.sh` | Commit view | +| `t0106-diff.sh` | Diff view | +| `t0107-snapshot.sh` | Snapshot downloads | +| `t0108-patch.sh` | Patch view | +| `t0109-gitconfig.sh` | Git config integration | +| `t0110-rawdiff.sh` | Raw diff output | +| `t0111-filter.sh` | Filter system | +| `t0112-coc.sh` | Code of Conduct page | +| `t0113-cla.sh` | CLA page | +| `t0114-root-homepage.sh` | Root homepage links | + +### Number Ranges + +| Range | Category | +|-------|----------| +| `t0001-t0099` | Infrastructure/validation tests | +| `t0100-t0199` | Feature tests | + +## Running Tests + +### All Tests + +```bash +cd cgit/tests +make +``` + +The Makefile discovers all `t*.sh` files and runs them: + +```makefile +T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) + +all: $(T) + +$(T): + @'$(SHELL_PATH_SQ)' $@ $(CGIT_TEST_OPTS) +``` + +### Individual Tests + +```bash +# Run a single test +./t0101-index.sh + +# With verbose output +./t0101-index.sh -v + +# With Valgrind +./t0101-index.sh --valgrind +``` + +### Test Options + +Options are passed via `CGIT_TEST_OPTS` or command-line arguments: + +| Option | Description | +|--------|-------------| +| `-v`, `--verbose` | Show test details | +| `--valgrind` | Run cgit under Valgrind | +| `--debug` | Show shell trace | + +### Valgrind Support + +`setup.sh` intercepts the `--valgrind` flag and configures Valgrind +instrumentation via a wrapper script in `tests/valgrind/`: + +```bash +if test -n "$cgit_valgrind"; then + GIT_VALGRIND="$TEST_DIRECTORY/valgrind" + CGIT_VALGRIND=$(cd ../valgrind && pwd) + PATH="$CGIT_VALGRIND/bin:$PATH" +fi +``` + +## Test Patterns + +### HTML Content Assertion + +```bash +run_test 'repo index contains foo' ' + cgit_url "/" | strip_headers | grep -q "foo" +' +``` + +### HTTP Header Assertion + +```bash +run_test 'content type is text/html' ' + cgit_url "/" | head -1 | grep -q "Content-Type: text/html" +' +``` + +### Snapshot Download + +```bash +run_test 'snapshot is valid tar.gz' ' + cgit_url "/foo/snapshot/foo-master.tar.gz" | strip_headers | \ + gunzip | tar tf - >/dev/null +' +``` + +### Negative Assertion + +```bash +run_test 'no 404 on valid repo' ' + ! cgit_url "/foo" | grep -q "404" +' +``` + +### Lua Filter Conditional + +```bash +if [ $CGIT_HAS_LUA -eq 1 ]; then + run_test 'lua filter works' ' + cgit_url "/filter-lua/about/" | strip_headers | grep -q "filtered" + ' +fi +``` + +## Test Filter Scripts + +The `tests/filters/` directory contains simple filter scripts for testing: + +### `dump.sh` + +A passthrough filter that copies stdin to stdout, used to verify filter +invocation: + +```bash +#!/bin/sh +cat +``` + +### `dump.lua` + +Lua equivalent of the dump filter: + +```lua +function filter_open(...) +end + +function write(str) + html(str) +end + +function filter_close() + return 0 +end +``` + +## Cleanup + +```bash +cd cgit/tests +make clean +``` + +Removes the `trash` directories created by tests. + +## Writing New Tests + +1. Create a new file `tNNNN-description.sh` +2. Source `setup.sh` and call `prepare_tests`: + +```bash +#!/bin/sh +. ./setup.sh +prepare_tests "my new feature" + +run_test 'description of test case' ' + cgit_url "/foo/my-page/" | strip_headers | grep -q "expected" +' +``` + +3. Make it executable: `chmod +x tNNNN-description.sh` +4. Run: `./tNNNN-description.sh -v` + +## CI Integration + +Tests are run as part of the CI pipeline. The `ci/` directory contains +Nix-based CI configuration that builds cgit and runs the test suite in a +reproducible environment. diff --git a/docs/handbook/cgit/ui-modules.md b/docs/handbook/cgit/ui-modules.md new file mode 100644 index 0000000000..b03a437a35 --- /dev/null +++ b/docs/handbook/cgit/ui-modules.md @@ -0,0 +1,544 @@ +# cgit — UI Modules + +## Overview + +cgit's user interface is implemented as a collection of `ui-*.c` modules, +each responsible for rendering a specific page type. All modules share +common infrastructure from `ui-shared.c` and `html.c`. + +## Module Map + +| Module | Page | Entry Function | +|--------|------|---------------| +| `ui-repolist.c` | Repository index | `cgit_print_repolist()` | +| `ui-summary.c` | Repository summary | `cgit_print_summary()` | +| `ui-log.c` | Commit log | `cgit_print_log()` | +| `ui-tree.c` | File/directory tree | `cgit_print_tree()` | +| `ui-blob.c` | File content | `cgit_print_blob()` | +| `ui-commit.c` | Commit details | `cgit_print_commit()` | +| `ui-diff.c` | Diff view | `cgit_print_diff()` | +| `ui-ssdiff.c` | Side-by-side diff | `cgit_ssdiff_*()` | +| `ui-patch.c` | Patch output | `cgit_print_patch()` | +| `ui-refs.c` | Branch/tag listing | `cgit_print_refs()` | +| `ui-tag.c` | Tag details | `cgit_print_tag()` | +| `ui-stats.c` | Statistics | `cgit_print_stats()` | +| `ui-atom.c` | Atom feed | `cgit_print_atom()` | +| `ui-plain.c` | Raw file serving | `cgit_print_plain()` | +| `ui-blame.c` | Blame view | `cgit_print_blame()` | +| `ui-clone.c` | HTTP clone | `cgit_clone_info/objects/head()` | +| `ui-snapshot.c` | Archive download | `cgit_print_snapshot()` | +| `ui-shared.c` | Common layout | (shared functions) | + +## `ui-repolist.c` — Repository Index + +Renders the main page listing all configured repositories. + +### Functions + +```c +void cgit_print_repolist(void) +``` + +### Features + +- Sortable columns: Name, Description, Owner, Idle (age) +- Section grouping (based on `repo.section` or `section-from-path`) +- Pagination with configurable `max-repo-count` +- Age calculation via `read_agefile()` or ref modification time +- Optional filter by search query + +### Sorting + +```c +static int cmp_name(const void *a, const void *b); +static int cmp_section(const void *a, const void *b); +static int cmp_idle(const void *a, const void *b); +``` + +Sort field is selected by the `s` query parameter or `repository-sort` +directive. + +### Age File Resolution + +```c +static time_t read_agefile(const char *path) +{ + /* Try reading date from agefile content */ + /* Fall back to file mtime */ + /* Fall back to refs/ dir mtime */ +} +``` + +### Pagination + +```c +static void print_pager(int items, int pagelen, char *search, char *sort) +{ + /* Render page navigation links */ + /* [prev] 1 2 3 4 5 [next] */ +} +``` + +## `ui-summary.c` — Repository Summary + +Renders the overview page for a single repository. + +### Functions + +```c +void cgit_print_summary(void) +``` + +### Content + +- Repository metadata table (description, owner, homepage, clone URLs) +- SPDX license detection from `LICENSES/` directory +- CODEOWNERS and MAINTAINERS file detection +- Badges display +- Branch listing (limited by `summary-branches`) +- Tag listing (limited by `summary-tags`) +- Recent commits (limited by `summary-log`) +- Snapshot download links +- README rendering (via about-filter) + +### License Detection + +```c +/* Scan for SPDX license identifiers */ +/* Check LICENSES/ directory for .txt files */ +/* Extract license names from filenames */ +``` + +### README Priority + +README files are tried in order of `repo.readme` entries: + +1. `ref:README.md` — tracked file in a specific ref +2. `:README.md` — tracked file in HEAD +3. `/path/to/README.md` — file on disk + +## `ui-log.c` — Commit Log + +Renders a paginated list of commits. + +### Functions + +```c +void cgit_print_log(const char *tip, int ofs, int cnt, + char *grep, char *pattern, char *path, + int pager, int commit_graph, int commit_sort) +``` + +### Features + +- Commit graph visualization (ASCII art) +- File change count per commit (when `enable-log-filecount=1`) +- Line count per commit (when `enable-log-linecount=1`) +- Grep/search within commit messages +- Path filtering (show commits affecting a specific path) +- Follow renames (when `enable-follow-links=1`) +- Pagination with next/prev links + +### Commit Graph Colors + +```c +static const char *column_colors_html[] = { + "<span class='column1'>", + "<span class='column2'>", + "<span class='column3'>", + "<span class='column4'>", + "<span class='column5'>", + "<span class='column6'>", +}; +``` + +### Decorations + +```c +static void show_commit_decorations(struct commit *commit) +{ + /* Display branch/tag labels next to commits */ + /* Uses git's decoration API */ +} +``` + +## `ui-tree.c` — Tree View + +Renders directory listings and file contents. + +### Functions + +```c +void cgit_print_tree(const char *rev, char *path) +``` + +### Directory Listing + +For each entry in a tree object: + +```c +/* For each tree entry */ +switch (entry->mode) { + case S_IFDIR: /* directory → link to subtree */ + case S_IFREG: /* regular file → link to blob */ + case S_IFLNK: /* symlink → show target */ + case S_IFGITLINK: /* submodule → link to submodule */ +} +``` + +### File Display + +```c +static void print_text_buffer(const char *name, char *buf, + unsigned long size) +{ + /* Show file content with line numbers */ + /* Apply source filter if configured */ +} + +static void print_binary_buffer(char *buf, unsigned long size) +{ + /* Show "Binary file (N bytes)" message */ +} +``` + +### Walk Tree Context + +```c +struct walk_tree_context { + char *curr_rev; + char *match_path; + int state; /* 0=searching, 1=found, 2=printed */ +}; +``` + +The tree walker recursively descends into subdirectories to find the +requested path. + +## `ui-blob.c` — Blob View + +Displays individual file content or serves raw file data. + +### Functions + +```c +void cgit_print_blob(const char *hex, char *path, + const char *head, int file_only) +int cgit_ref_path_exists(const char *path, const char *ref, int file_only) +char *cgit_ref_read_file(const char *path, const char *ref, + unsigned long *size) +``` + +### MIME Detection + +When serving raw content, MIME types are detected from: +1. The `mimetype.<ext>` configuration directives +2. The `mimetype-file` (Apache-style mime.types) +3. Default: `application/octet-stream` + +## `ui-commit.c` — Commit View + +Displays full commit details. + +### Functions + +```c +void cgit_print_commit(const char *rev, const char *prefix) +``` + +### Content + +- Author and committer info (name, email, date) +- Commit subject and full message +- Parent commit links +- Git notes +- Commit decorations (branches, tags) +- Diffstat +- Full diff (unified or side-by-side) + +### Notes Display + +```c +/* Check for git notes */ +struct strbuf notes = STRBUF_INIT; +format_display_notes(&commit->object.oid, ¬es, ...); +if (notes.len) { + html("<div class='notes-header'>Notes</div>"); + html("<div class='notes'>"); + html_txt(notes.buf); + html("</div>"); +} +``` + +## `ui-diff.c` — Diff View + +Renders diffs between commits or trees. + +### Functions + +```c +void cgit_print_diff(const char *new_rev, const char *old_rev, + const char *prefix, int show_ctrls, int raw) +void cgit_print_diffstat(const struct object_id *old, + const struct object_id *new, + const char *prefix) +``` + +See [diff-engine.md](diff-engine.md) for detailed documentation. + +## `ui-ssdiff.c` — Side-by-Side Diff + +Renders two-column diff view with character-level highlighting. + +### Functions + +```c +void cgit_ssdiff_header_begin(void) +void cgit_ssdiff_header_end(void) +void cgit_ssdiff_footer(void) +``` + +See [diff-engine.md](diff-engine.md) for LCS algorithm details. + +## `ui-patch.c` — Patch Output + +Generates a downloadable patch file. + +### Functions + +```c +void cgit_print_patch(const char *new_rev, const char *old_rev, + const char *prefix) +``` + +Output is `text/plain` content suitable for `git apply`. Uses Git's +`rev_info` and `log_tree_commit` to generate the patch. + +## `ui-refs.c` — References View + +Displays branches and tags with sorting. + +### Functions + +```c +void cgit_print_refs(void) +void cgit_print_branches(int max) +void cgit_print_tags(int max) +``` + +### Branch Display + +Each branch row shows: +- Branch name (link to log) +- Idle time +- Author of last commit + +### Tag Display + +Each tag row shows: +- Tag name (link to tag) +- Idle time +- Author/tagger +- Download links (if snapshots enabled) + +### Sorting + +```c +static int cmp_branch_age(const void *a, const void *b); +static int cmp_tag_age(const void *a, const void *b); +static int cmp_branch_name(const void *a, const void *b); +static int cmp_tag_name(const void *a, const void *b); +``` + +Sort order is controlled by `branch-sort` (0=name, 1=age). + +## `ui-tag.c` — Tag View + +Displays details of a specific tag. + +### Functions + +```c +void cgit_print_tag(const char *revname) +``` + +### Content + +For annotated tags: +- Tagger name and date +- Tag message +- Tagged object link + +For lightweight tags: +- Redirects to the tagged object (commit, tree, or blob) + +## `ui-stats.c` — Statistics View + +Displays contributor statistics by period. + +### Functions + +```c +void cgit_print_stats(void) +``` + +### Periods + +```c +struct cgit_period { + const char *name; /* "week", "month", "quarter", "year" */ + int max_periods; + int count; + /* accessor functions for period boundaries */ +}; +``` + +### Data Collection + +```c +static void collect_stats(struct cgit_period *period) +{ + /* Walk commit log */ + /* Group commits by author and period */ + /* Count additions/deletions per period */ +} +``` + +### Output + +- Bar chart showing commits per period +- Author ranking table +- Sortable by commit count + +## `ui-atom.c` — Atom Feed + +Generates an Atom XML feed. + +### Functions + +```c +void cgit_print_atom(char *tip, char *path, int max) +``` + +### Output + +```xml +<?xml version='1.0' encoding='utf-8'?> +<feed xmlns='http://www.w3.org/2005/Atom'> + <title>repo - log</title> + <updated>2024-01-01T00:00:00Z</updated> + <entry> + <title>commit subject</title> + <updated>2024-01-01T00:00:00Z</updated> + <author><name>Alice</name><email>alice@example.com</email></author> + <id>urn:sha1:abc123</id> + <link href='commit URL'/> + <content type='text'>commit message</content> + </entry> +</feed> +``` + +Limited by `max-atom-items` (default 10). + +## `ui-plain.c` — Raw File Serving + +Serves file content with proper MIME types. + +### Functions + +```c +void cgit_print_plain(void) +``` + +### Features + +- MIME type detection by file extension +- Directory listing (HTML) when path is a tree +- Binary file serving with correct Content-Type +- Security: HTML serving gated by `enable-html-serving` + +### Security + +When `enable-html-serving=0` (default), HTML files are served as +`text/plain` to prevent XSS. + +## `ui-blame.c` — Blame View + +Displays line-by-line blame information. + +### Functions + +```c +void cgit_print_blame(void) +``` + +### Implementation + +Uses Git's `blame_scoreboard` API: + +```c +/* Set up blame scoreboard */ +/* Walk file history */ +/* For each line, emit: commit hash, author, line content */ +``` + +### Output + +Each line shows: +- Abbreviated commit hash (linked to commit view) +- Line number +- File content + +Requires `enable-blame=1`. + +## `ui-clone.c` — HTTP Clone Endpoints + +Serves the smart HTTP clone protocol. + +### Functions + +```c +void cgit_clone_info(void) /* GET info/refs */ +void cgit_clone_objects(void) /* GET objects/* */ +void cgit_clone_head(void) /* GET HEAD */ +``` + +### `cgit_clone_info()` + +Enumerates all refs and their SHA-1 hashes: + +```c +static void print_ref_info(const char *refname, + const struct object_id *oid, ...) +{ + /* Output: sha1\trefname\n */ +} +``` + +### `cgit_clone_objects()` + +Serves loose objects and pack files from the object store. + +### `cgit_clone_head()` + +Returns the symbolic HEAD reference. + +Requires `enable-http-clone=1` (default). + +## `ui-snapshot.c` — Archive Downloads + +See [snapshot-system.md](snapshot-system.md) for detailed documentation. + +## `ui-shared.c` — Common Infrastructure + +Provides shared layout and link generation used by all modules. + +See [html-rendering.md](html-rendering.md) for detailed documentation. + +### Key Functions + +- Page skeleton: `cgit_print_docstart()`, `cgit_print_pageheader()`, + `cgit_print_docend()` +- Links: `cgit_commit_link()`, `cgit_tree_link()`, `cgit_log_link()`, etc. +- URLs: `cgit_repourl()`, `cgit_fileurl()`, `cgit_pageurl()` +- Errors: `cgit_print_error_page()` diff --git a/docs/handbook/cgit/url-routing.md b/docs/handbook/cgit/url-routing.md new file mode 100644 index 0000000000..0adb3b7fc5 --- /dev/null +++ b/docs/handbook/cgit/url-routing.md @@ -0,0 +1,331 @@ +# cgit — URL Routing and Request Dispatch + +## Overview + +cgit supports two URL schemes: virtual-root (path-based) and query-string. +Incoming requests are parsed into a `cgit_query` structure and dispatched to +one of 23 command handlers via a function pointer table. + +Source files: `cgit.c` (querystring parsing, routing), `parsing.c` +(`cgit_parse_url`), `cmd.c` (command table). + +## URL Schemes + +### Virtual Root (Path-Based) + +When `virtual-root` is configured, URLs use clean paths: + +``` +/cgit/ → repository list +/cgit/repo.git/ → summary +/cgit/repo.git/log/ → log (default branch) +/cgit/repo.git/log/main/path → log for path on branch main +/cgit/repo.git/tree/v1.0/src/ → tree view at tag v1.0 +/cgit/repo.git/commit/?id=abc → commit view +``` + +The path after the virtual root is passed in `PATH_INFO` and parsed by +`cgit_parse_url()`. + +### Query-String (CGI) + +Without virtual root, all parameters are passed in the query string: + +``` +/cgit.cgi?url=repo.git/log/main/path&ofs=50 +``` + +## Query Structure + +All parsed parameters are stored in `ctx.qry`: + +```c +struct cgit_query { + char *raw; /* raw URL / PATH_INFO */ + char *repo; /* repository URL */ + char *page; /* page/command name */ + char *search; /* search string */ + char *grep; /* grep pattern */ + char *head; /* branch reference */ + char *sha1; /* object SHA-1 */ + char *sha2; /* second SHA-1 (for diffs) */ + char *path; /* file/dir path within repo */ + char *name; /* snapshot name / ref name */ + char *url; /* combined URL path */ + char *mimetype; /* requested MIME type */ + char *etag; /* ETag from client */ + int nohead; /* suppress header */ + int ofs; /* pagination offset */ + int has_symref; /* path contains a symbolic ref */ + int has_sha1; /* explicit SHA was given */ + int has_dot; /* path contains '..' */ + int ignored; /* request should be ignored */ + char *sort; /* sort field */ + int showmsg; /* show full commit message */ + int ssdiff; /* side-by-side diff */ + int show_all; /* show all items */ + int context; /* diff context lines */ + int follow; /* follow renames */ + int log_hierarchical_threading; +}; +``` + +## URL Parsing: `cgit_parse_url()` + +In `parsing.c`, the URL is decomposed into repo, page, and path: + +```c +void cgit_parse_url(const char *url) +{ + /* Step 1: try progressively longer prefixes as repo URLs */ + /* For each '/' in the URL, check if the prefix matches a repo */ + + for (p = strchr(url, '/'); p; p = strchr(p + 1, '/')) { + *p = '\0'; + repo = cgit_get_repoinfo(url); + *p = '/'; + if (repo) { + ctx.qry.repo = xstrdup(url_prefix); + ctx.repo = repo; + url = p + 1; /* remaining part */ + break; + } + } + /* if no '/' found, try the whole URL as a repo name */ + + /* Step 2: parse the remaining path as page/ref/path */ + /* e.g., "log/main/src/file.c" → page="log", path="main/src/file.c" */ + p = strchr(url, '/'); + if (p) { + ctx.qry.page = xstrndup(url, p - url); + ctx.qry.path = trim_end(p + 1, '/'); + } else if (*url) { + ctx.qry.page = xstrdup(url); + } +} +``` + +## Query String Parsing: `querystring_cb()` + +HTTP query parameters and POST form data are decoded by `querystring_cb()` +in `cgit.c`. The function maps URL parameter names to `ctx.qry` fields: + +```c +static void querystring_cb(const char *name, const char *value) +{ + if (!strcmp(name, "url")) ctx.qry.url = xstrdup(value); + else if (!strcmp(name, "p")) ctx.qry.page = xstrdup(value); + else if (!strcmp(name, "q")) ctx.qry.search = xstrdup(value); + else if (!strcmp(name, "h")) ctx.qry.head = xstrdup(value); + else if (!strcmp(name, "id")) ctx.qry.sha1 = xstrdup(value); + else if (!strcmp(name, "id2")) ctx.qry.sha2 = xstrdup(value); + else if (!strcmp(name, "ofs")) ctx.qry.ofs = atoi(value); + else if (!strcmp(name, "path")) ctx.qry.path = xstrdup(value); + else if (!strcmp(name, "name")) ctx.qry.name = xstrdup(value); + else if (!strcmp(name, "mimetype")) ctx.qry.mimetype = xstrdup(value); + else if (!strcmp(name, "s")) ctx.qry.sort = xstrdup(value); + else if (!strcmp(name, "showmsg")) ctx.qry.showmsg = atoi(value); + else if (!strcmp(name, "ss")) ctx.qry.ssdiff = atoi(value); + else if (!strcmp(name, "all")) ctx.qry.show_all = atoi(value); + else if (!strcmp(name, "context")) ctx.qry.context = atoi(value); + else if (!strcmp(name, "follow")) ctx.qry.follow = atoi(value); + else if (!strcmp(name, "dt")) ctx.qry.dt = atoi(value); + else if (!strcmp(name, "grep")) ctx.qry.grep = xstrdup(value); + else if (!strcmp(name, "etag")) ctx.qry.etag = xstrdup(value); +} +``` + +### URL Parameter Reference + +| Parameter | Query Field | Type | Description | +|-----------|------------|------|-------------| +| `url` | `qry.url` | string | Full URL path (repo/page/path) | +| `p` | `qry.page` | string | Page/command name | +| `q` | `qry.search` | string | Search string | +| `h` | `qry.head` | string | Branch/ref name | +| `id` | `qry.sha1` | string | Object SHA-1 | +| `id2` | `qry.sha2` | string | Second SHA-1 (diffs) | +| `ofs` | `qry.ofs` | int | Pagination offset | +| `path` | `qry.path` | string | File path in repo | +| `name` | `qry.name` | string | Reference/snapshot name | +| `mimetype` | `qry.mimetype` | string | MIME type override | +| `s` | `qry.sort` | string | Sort field | +| `showmsg` | `qry.showmsg` | int | Show full commit message | +| `ss` | `qry.ssdiff` | int | Side-by-side diff toggle | +| `all` | `qry.show_all` | int | Show all entries | +| `context` | `qry.context` | int | Diff context lines | +| `follow` | `qry.follow` | int | Follow renames in log | +| `dt` | `qry.dt` | int | Diff type | +| `grep` | `qry.grep` | string | Grep pattern for log search | +| `etag` | `qry.etag` | string | ETag for conditional requests | + +## Command Dispatch Table + +The command table in `cmd.c` maps page names to handler functions: + +```c +#define def_cmd(name, want_hierarchical, want_repo, want_layout, want_vpath, is_clone) \ + {#name, cmd_##name, want_hierarchical, want_repo, want_layout, want_vpath, is_clone} + +static struct cgit_cmd cmds[] = { + def_cmd(atom, 1, 1, 0, 0, 0), + def_cmd(about, 0, 1, 1, 0, 0), + def_cmd(blame, 1, 1, 1, 1, 0), + def_cmd(blob, 1, 1, 0, 0, 0), + def_cmd(commit, 1, 1, 1, 1, 0), + def_cmd(diff, 1, 1, 1, 1, 0), + def_cmd(head, 1, 1, 0, 0, 1), + def_cmd(info, 1, 1, 0, 0, 1), + def_cmd(log, 1, 1, 1, 1, 0), + def_cmd(ls_cache,0, 0, 0, 0, 0), + def_cmd(objects, 1, 1, 0, 0, 1), + def_cmd(patch, 1, 1, 1, 1, 0), + def_cmd(plain, 1, 1, 0, 1, 0), + def_cmd(rawdiff, 1, 1, 0, 1, 0), + def_cmd(refs, 1, 1, 1, 0, 0), + def_cmd(repolist,0, 0, 1, 0, 0), + def_cmd(snapshot, 1, 1, 0, 0, 0), + def_cmd(stats, 1, 1, 1, 1, 0), + def_cmd(summary, 1, 1, 1, 0, 0), + def_cmd(tag, 1, 1, 1, 0, 0), + def_cmd(tree, 1, 1, 1, 1, 0), +}; +``` + +### Command Flags + +| Flag | Meaning | +|------|---------| +| `want_hierarchical` | Parse hierarchical path from URL | +| `want_repo` | Requires a repository context | +| `want_layout` | Render within HTML page layout | +| `want_vpath` | Accept a virtual path (file path in repo) | +| `is_clone` | HTTP clone protocol endpoint | + +### Lookup: `cgit_get_cmd()` + +```c +struct cgit_cmd *cgit_get_cmd(const char *name) +{ + for (int i = 0; i < ARRAY_SIZE(cmds); i++) + if (!strcmp(cmds[i].name, name)) + return &cmds[i]; + return NULL; +} +``` + +The function performs a linear search. With 21 entries, this is fast enough. + +## Request Processing Flow + +In `process_request()` (`cgit.c`): + +``` +1. Parse PATH_INFO via cgit_parse_url() +2. Parse QUERY_STRING via http_parse_querystring(querystring_cb) +3. Parse POST body (for authentication forms) +4. Resolve repository: cgit_get_repoinfo(ctx.qry.repo) +5. Determine command: cgit_get_cmd(ctx.qry.page) +6. If no page specified: + - With repo → default to "summary" + - Without repo → default to "repolist" +7. Check command flags: + - want_repo but no repo → "Repository not found" error + - is_clone and HTTP clone disabled → 404 +8. Handle authentication if auth-filter is configured +9. Execute: cmd->fn(&ctx) +``` + +### Hierarchical Path Resolution + +When `want_hierarchical=1`, cgit splits `ctx.qry.path` into a reference +(branch/tag/SHA) and a file path. It tries progressively longer prefixes +of the path as git references until one resolves: + +``` +path = "main/src/lib/file.c" +try: "main" → found branch "main" + qry.head = "main" + qry.path = "src/lib/file.c" +``` + +If no prefix resolves, the entire path is treated as a file path within the +default branch. + +## Clone Protocol Endpoints + +Three commands serve the Git HTTP clone protocol: + +| Endpoint | Path | Function | +|----------|------|----------| +| `info` | `repo/info/refs` | `cgit_clone_info()` — advertise refs | +| `objects` | `repo/objects/*` | `cgit_clone_objects()` — serve packfiles | +| `head` | `repo/HEAD` | `cgit_clone_head()` — serve HEAD ref | + +These are only active when `enable-http-clone=1` (default). + +## URL Generation + +`ui-shared.c` provides URL construction helpers: + +```c +const char *cgit_repourl(const char *reponame); +const char *cgit_fileurl(const char *reponame, const char *pagename, + const char *filename, const char *query); +const char *cgit_pageurl(const char *reponame, const char *pagename, + const char *query); +const char *cgit_currurl(void); +``` + +When `virtual-root` is set, these produce clean paths. Otherwise, they +produce query-string URLs. + +### Example URL generation: + +```c +/* With virtual-root=/cgit/ */ +cgit_repourl("myrepo") + → "/cgit/myrepo/" + +cgit_fileurl("myrepo", "tree", "src/main.c", "h=dev") + → "/cgit/myrepo/tree/src/main.c?h=dev" + +cgit_pageurl("myrepo", "log", "ofs=50") + → "/cgit/myrepo/log/?ofs=50" +``` + +## Content-Type and HTTP Headers + +The response content type is set by the command handler before generating +output. Common types: + +| Page | Content-Type | +|------|-------------| +| HTML pages | `text/html` | +| atom | `text/xml` | +| blob | auto-detected from content | +| plain | MIME type from extension or `application/octet-stream` | +| snapshot | `application/x-gzip`, etc. | +| patch | `text/plain` | +| clone endpoints | `text/plain`, `application/x-git-packed-objects` | + +Headers are emitted by `cgit_print_http_headers()` in `ui-shared.c` before +any page content. + +## Error Handling + +If a requested repository or page is not found, cgit renders an error page +within the standard layout. HTTP status codes: + +| Condition | Status | +|-----------|--------| +| Normal page | 200 OK | +| Auth redirect | 302 Found | +| Not modified | 304 Not Modified | +| Bad request | 400 Bad Request | +| Auth required | 401 Unauthorized | +| Repo not found | 404 Not Found | +| Page not found | 404 Not Found | + +The status code is set in `ctx.page.status` and emitted by the HTTP header +function. |
