Diff options
Diffstat (limited to '')
| -rw-r--r-- | source/cgit.c | 320 |
1 file changed, 141 insertions, 179 deletions
diff --git a/source/cgit.c b/source/cgit.c index eaf1d19..f8795d7 100644 --- a/source/cgit.c +++ b/source/cgit.c @@ -42,10 +42,6 @@ // every row it skips, so this bounds the work an offset alone can buy. #define MAX_QUERY_OFFSET 100000 -// cgit knows fewer than eight archive formats, so this stands in for every bit -// a snapshots mask can carry. -#define ALL_SNAPSHOT_FORMATS 0xFF - // The first branch found is the fallback, so a repository whose default branch // does not exist still has something to show. struct refmatch { @@ -89,11 +85,19 @@ static NORETURN void die_routine(const char *msg, va_list params) exit(0); } +static char *ensure_end(const char *str, char c) +{ + size_t len = strlen(str); + + return len && str[len - 1] == c ? xstrdup(str) : xstrfmt("%s%c", str, c); +} + static void prepare_context(void) { + const char *script_name = getenv("SCRIPT_NAME"); + memset(&ctx, 0, sizeof(ctx)); ctx.cfg.agefile = "info/web/last-modified"; - ctx.cfg.cache_size = 0; ctx.cfg.cache_root = CGIT_CACHE_ROOT; ctx.cfg.cache_about_ttl = 15; ctx.cfg.cache_snapshot_ttl = 5; @@ -106,11 +110,8 @@ static void prepare_context(void) // is served but not kept. ctx.cfg.cache_max_slot_size = 64 * 1024; ctx.cfg.case_sensitive_sort = 1; - ctx.cfg.branch_sort = 0; - ctx.cfg.commit_sort = 0; ctx.cfg.logo = "/cgit.png"; ctx.cfg.favicon = "/favicon.ico"; - ctx.cfg.local_time = 0; ctx.cfg.date_mode = date_mode_from_type(DATE_SHORT); ctx.cfg.enable_header = 1; ctx.cfg.enable_mailmap = 1; @@ -119,7 +120,6 @@ static void prepare_context(void) ctx.cfg.enable_http_clone = 1; ctx.cfg.enable_index_owner = 1; ctx.cfg.enable_tree_linenumbers = 1; - ctx.cfg.enable_git_config = 0; ctx.cfg.max_repo_count = 50; ctx.cfg.max_commit_count = 50; ctx.cfg.max_patch_count = 50; @@ -131,23 +131,16 @@ static void prepare_context(void) // Counted in kilobytes, so ten megabytes, which bounds the memory one // request can be made to allocate for a blob. ctx.cfg.max_blob_size = 10 * 1024; - ctx.cfg.max_stats = 0; - ctx.cfg.project_list = NULL; ctx.cfg.renamelimit = -1; - ctx.cfg.remove_suffix = 0; ctx.cfg.robots = "index, nofollow"; ctx.cfg.root_title = "Git repository browser"; ctx.cfg.root_desc = "a fast webinterface for the git dscm"; - ctx.cfg.scan_hidden_path = 0; - ctx.cfg.script_name = CGIT_SCRIPT_NAME; ctx.cfg.section = ""; ctx.cfg.repository_sort = "name"; - ctx.cfg.section_sort = 0; ctx.cfg.summary_branches = 10; ctx.cfg.summary_log = 10; ctx.cfg.summary_tags = 10; ctx.cfg.max_atom_items = 10; - ctx.cfg.difftype = DIFF_UNIFIED; ctx.env.cgit_config = getenv("CGIT_CONFIG"); ctx.env.http_host = getenv("HTTP_HOST"); ctx.env.https = getenv("HTTPS"); @@ -155,21 +148,16 @@ static void prepare_context(void) ctx.env.path_info = getenv("PATH_INFO"); ctx.env.query_string = getenv("QUERY_STRING"); ctx.env.request_method = getenv("REQUEST_METHOD"); - ctx.env.script_name = getenv("SCRIPT_NAME"); ctx.env.server_name = getenv("SERVER_NAME"); ctx.env.server_port = getenv("SERVER_PORT"); ctx.env.http_cookie = getenv("HTTP_COOKIE"); ctx.env.http_referer = getenv("HTTP_REFERER"); ctx.env.content_length = getenv("CONTENT_LENGTH") ? strtoul(getenv("CONTENT_LENGTH"), NULL, 10) : 0; - ctx.env.authenticated = 0; ctx.page.mimetype = "text/html"; ctx.page.charset = PAGE_ENCODING; - ctx.page.filename = NULL; - ctx.page.size = 0; string_list_init_dup(&ctx.cfg.mimetypes); - if (ctx.env.script_name) - ctx.cfg.script_name = xstrdup(ctx.env.script_name); + ctx.cfg.virtual_root = ensure_end(script_name ? script_name : CGIT_SCRIPT_NAME, '/'); if (ctx.env.query_string) ctx.qry.raw = xstrdup(ctx.env.query_string); if (!ctx.env.cgit_config) @@ -178,7 +166,7 @@ static void prepare_context(void) static void print_version(void) { - printf("CGit %s | https://github.com/brycekwon/cgit\n\nCompiled in features:\n", CGIT_VERSION); + printf("cgit %s | https://github.com/brycekwon/cgit\n\nCompiled in features:\n", CGIT_VERSION); #ifdef NO_LUA printf("[-] "); #else @@ -262,7 +250,7 @@ static void print_repo(FILE *f, struct cgit_repo *repo) cgit_fprintf_filter(repo->trailer_filter, f, "repo.trailer-filter="); if (repo->snapshots != ctx.cfg.snapshots) { char *formats = build_snapshot_setting(repo->snapshots); - fprintf(f, "repo.snapshots=%s\n", formats ? formats : ""); + fprintf(f, "repo.snapshots=%s\n", formats); free(formats); } if (repo->snapshot_prefix) @@ -279,12 +267,10 @@ static void print_repo(FILE *f, struct cgit_repo *repo) fprintf(f, "repo.enable-html-serving=%d\n", repo->enable_html_serving); if (repo->branch_sort == 1) fprintf(f, "repo.branch-sort=age\n"); - if (repo->commit_sort) { - if (repo->commit_sort == 1) - fprintf(f, "repo.commit-sort=date\n"); - else if (repo->commit_sort == 2) - fprintf(f, "repo.commit-sort=topo\n"); - } + if (repo->commit_sort == 1) + fprintf(f, "repo.commit-sort=date\n"); + else if (repo->commit_sort == 2) + fprintf(f, "repo.commit-sort=topo\n"); fprintf(f, "repo.hide=%d\n", repo->hide); fprintf(f, "repo.ignore=%d\n", repo->ignore); fprintf(f, "\n"); @@ -315,17 +301,6 @@ static void parse_args(int argc, const char **argv) ctx.env.no_http = "1"; } else if (skip_prefix(argv[i], "--query=", &arg)) { ctx.qry.raw = xstrdup(arg); - } else if (skip_prefix(argv[i], "--repo=", &arg)) { - ctx.qry.repo = xstrdup(arg); - } else if (skip_prefix(argv[i], "--page=", &arg)) { - ctx.qry.page = xstrdup(arg); - } else if (skip_prefix(argv[i], "--head=", &arg)) { - ctx.qry.head = xstrdup(arg); - } else if (skip_prefix(argv[i], "--oid=", &arg)) { - ctx.qry.oid = xstrdup(arg); - ctx.qry.has_oid = 1; - } else if (skip_prefix(argv[i], "--ofs=", &arg)) { - ctx.qry.ofs = atoi(arg); } else if ( skip_prefix(argv[i], "--scan-tree=", &arg) || skip_prefix(argv[i], "--scan-path=", &arg) @@ -333,73 +308,36 @@ static void parse_args(int argc, const char **argv) // A repository's snapshots setting is masked with the // global one, and cgitrc has not been read here, so an // empty mask would discard what the repository set. - ctx.cfg.snapshots = ALL_SNAPSHOT_FORMATS; + ctx.cfg.snapshots = INT_MAX; scanned++; scan_tree(arg); } } if (scanned) { - cgit_qsort(cgit_repolist.repos, cgit_repolist.count, sizeof(struct cgit_repo), cmp_repos); + QSORT(cgit_repolist.repos, cgit_repolist.count, cmp_repos); print_repolist(stdout, &cgit_repolist, 0); exit(0); } } /* - * The lock is a fcntl lock rather than the mere existence of the lock file, + * The lock is an flock rather than the mere existence of the lock file, * because a lock the kernel drops with its process cannot outlive a scan * killed mid-run. A leftover file would count as a scan forever in progress. */ static int generate_cached_repolist(const char *path, const char *cached_rc) { struct strbuf locked_rc = STRBUF_INIT; - struct flock lock = { - .l_type = F_WRLCK, - .l_whence = SEEK_SET, - .l_start = 0, - .l_len = 0, - }; - struct stat held, named; int err = 0; int fd; int first; FILE *f; strbuf_addf(&locked_rc, "%s.lock", cached_rc); - fd = open(locked_rc.buf, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); - if (fd == -1) { - err = errno; - fprintf(stderr, "[cgit] Unable to open %s: %s (%d)\n", locked_rc.buf, strerror(err), err); - goto out; - } - if (fcntl(fd, F_SETLK, &lock) < 0) { - // A lock held elsewhere only means concurrent requests, which - // is not worth a line in the server log. Any other failure, a - // filesystem without lock support say, silently costs a scan - // on every request and does deserve one. - err = errno; - if (err != EACCES && err != EAGAIN) - fprintf(stderr, "[cgit] Unable to lock %s: %s (%d)\n", locked_rc.buf, strerror(err), err); - close(fd); - goto out; - } - // The lock landed on whatever inode the path named at open. A holder - // finishing in between renames that inode into place as the live - // list, and once the path is confirmed to still name this file that - // rename can no longer happen, because it takes the lock held here. - if ( - fstat(fd, &held) || - stat(locked_rc.buf, &named) || - held.st_ino != named.st_ino || - held.st_dev != named.st_dev - ) { - err = EAGAIN; - close(fd); + err = cache_lock_file(locked_rc.buf, &fd); + if (err) goto out; - } - // A run that died before its rename leaves the lock file behind, so - // start from empty now that nobody else can be writing it. - if (ftruncate(fd, 0) < 0 || !(f = fdopen(fd, "w"))) { + if (!(f = fdopen(fd, "w"))) { err = errno; fprintf(stderr, "[cgit] Unable to write %s: %s (%d)\n", locked_rc.buf, strerror(err), err); unlink(locked_rc.buf); @@ -407,10 +345,7 @@ static int generate_cached_repolist(const char *path, const char *cached_rc) goto out; } first = cgit_repolist.count; - if (ctx.cfg.project_list) - scan_projects(path, ctx.cfg.project_list); - else - scan_tree(path); + scan_tree(path); print_repolist(f, &cgit_repolist, first); // Flushed before the rename, because print_repolist writes through // stdio and a rename of the file would otherwise publish a repolist @@ -457,12 +392,8 @@ static void process_cached_repolist(const char *path) if (stat(cached_rc.buf, &st)) { // Nothing is cached yet, so this request scans in its own // process, leaving no copy behind when it cannot take the lock. - if (generate_cached_repolist(path, cached_rc.buf)) { - if (ctx.cfg.project_list) - scan_projects(path, ctx.cfg.project_list); - else - scan_tree(path); - } + if (generate_cached_repolist(path, cached_rc.buf)) + scan_tree(path); goto out; } @@ -504,6 +435,46 @@ static void add_mimetype(const char *name, const char *value) item->util = xstrdup(value); } +/* + * Accept only the date formats cgit documents, leaving mode unchanged for + * anything else. Git's parse_date_format dies on anything unknown, which would + * turn a cgitrc typo into a failed request. + */ +static void parse_date_setting(const char *format, struct date_mode *mode) +{ + static const char * const names[] = { + "default", "human", "iso", "iso-strict", "iso8601", + "iso8601-strict", "raw", "relative", "rfc", + "rfc2822", "short", "unix", + }; + const char *rest; + size_t i; + + if (skip_prefix(format, "format:", &rest)) { + // An empty strftime format would render every date blank. + if (*rest) + parse_date_format(format, mode); + return; + } + + // git reads local only as a whole alias of default-local, and dies on + // it with a suffix of its own. + if (!strcmp(format, "local")) { + parse_date_format(format, mode); + return; + } + + for (i = 0; i < ARRAY_SIZE(names); i++) { + if (!skip_prefix(format, names[i], &rest)) + continue; + // "iso" also prefixes "iso8601", so a partial hit is skipped. + if (*rest && strcmp(rest, "-local")) + continue; + parse_date_format(format, mode); + return; + } +} + static void apply_config(const char *name, const char *value) { const char *arg; @@ -545,7 +516,7 @@ static void apply_config(const char *name, const char *value) else if (!strcmp(name, "strict-export")) ctx.cfg.strict_export = cgit_strdup_first_line(value); else if (!strcmp(name, "virtual-root")) - ctx.cfg.virtual_root = cgit_ensure_end(value, '/'); + ctx.cfg.virtual_root = ensure_end(value, '/'); else if (!strcmp(name, "enable-plain-email")) ctx.cfg.enable_plain_email = atoi(value); else if (!strcmp(name, "enable-header")) @@ -657,8 +628,6 @@ static void apply_config(const char *name, const char *value) else if (!strcmp(name, "scan-path")) { if (ctx.cfg.cache_size) process_cached_repolist(cgit_expand_macros(value)); - else if (ctx.cfg.project_list) - scan_projects(cgit_expand_macros(value), ctx.cfg.project_list); else scan_tree(cgit_expand_macros(value)); // The scan grows the repository array, so a record taken before @@ -699,7 +668,7 @@ static void apply_config(const char *name, const char *value) else if (!strcmp(name, "local-time")) ctx.cfg.local_time = atoi(value); else if (!strcmp(name, "date-format")) - cgit_parse_date_format(value, &ctx.cfg.date_mode); + parse_date_setting(value, &ctx.cfg.date_mode); else if (!strcmp(name, "commit-sort")) { if (!strcmp(value, "date")) ctx.cfg.commit_sort = 1; @@ -738,9 +707,6 @@ static int query_int(const char *value, int min, int max) static void apply_query_param(const char *name, const char *value) { - if (!value) - value = ""; - if (!strcmp(name, "r")) { ctx.qry.repo = xstrdup(value); ctx.repo = cgit_get_repoinfo(value); @@ -757,10 +723,7 @@ static void apply_query_param(const char *name, const char *value) } else if (!strcmp(name, "qt")) { ctx.qry.grep = xstrdup(value); } else if (!strcmp(name, "q")) { - if (strlen(value) > MAX_SEARCH_LEN) - ctx.qry.search = xstrndup(value, MAX_SEARCH_LEN); - else - ctx.qry.search = xstrdup(value); + ctx.qry.search = xstrndup(value, MAX_SEARCH_LEN); } else if (!strcmp(name, "h")) { ctx.qry.head = xstrdup(value); } else if (!strcmp(name, "id")) { @@ -803,6 +766,23 @@ static void apply_query_param(const char *name, const char *value) } } +static void http_parse_querystring(const char *txt, void (*fn)(const char *name, const char *value)) +{ + const char *p = txt; + + while (p && *p) { + char *name = url_decode_parameter_name(&p); + // A name ends at an equals sign or an ampersand, and only the + // first is followed by a value. + if (*name) { + char *value = p[-1] == '=' ? url_decode_parameter_value(&p) : xstrdup(""); + fn(name, value); + free(value); + } + free(name); + } +} + static void open_auth_filter(const char *action) { cgit_open_filter(ctx.cfg.auth_filter, action, @@ -823,7 +803,7 @@ static void open_auth_filter(const char *action) * The filter answers the login POST itself, writing the status line and every * header, so nothing here prints any and the process ends before this returns. */ -static void authenticate_post(void) +static NORETURN void authenticate_post(void) { char buffer[MAX_AUTHENTICATION_POST_BYTES]; size_t len; @@ -856,10 +836,8 @@ static void authenticate_cookie(void) !ctx.repo && !strcmp(ctx.env.request_method, "POST") && !strcmp(ctx.qry.page, "login") - ) { + ) authenticate_post(); - return; - } open_auth_filter("authenticate-cookie"); ctx.env.authenticated = cgit_close_filter(ctx.cfg.auth_filter); @@ -884,33 +862,6 @@ static int is_full_oid(const char *rev) } /* - * Only these pages render the same bytes for the same id for ever. A log or - * refs page named with an id still lists branches and tags, which move. - */ -static int page_is_static(void) -{ - static const char *const pages[] = { - "blame", - "blob", - "commit", - "diff", - "patch", - "plain", - "rawdiff", - "snapshot", - "tag", - "tree", - }; - size_t i; - - for (i = 0; i < ARRAY_SIZE(pages); i++) { - if (!strcmp(ctx.qry.page, pages[i])) - return 1; - } - return 0; -} - -/* * Every cache-*-ttl setting is written in minutes, and so is this. */ static int calc_ttl(void) @@ -937,7 +888,8 @@ static int calc_ttl(void) // page to rebuild. if ( ctx.qry.has_oid && - page_is_static() && + cmd && + cmd->is_static && (!ctx.qry.oid || is_full_oid(ctx.qry.oid)) && (!ctx.qry.oid2 || is_full_oid(ctx.qry.oid2)) ) @@ -991,22 +943,13 @@ static int find_current_ref(const struct reference *ref, void *data) static char *find_default_branch(struct cgit_repo *repo) { - struct refmatch match; - char *ref; + struct refmatch match = { .wanted = repo->defbranch }; - match.wanted = repo->defbranch; - match.first = NULL; - match.found = 0; refs_for_each_branch_ref(get_main_ref_store(the_repository), find_current_ref, &match); - if (match.found) - ref = match.wanted; - else - ref = match.first; - if (ref) - ref = xstrdup(ref); + if (!match.found) + return match.first; free(match.first); - - return ref; + return xstrdup(match.wanted); } static char *guess_defbranch(void) @@ -1046,7 +989,7 @@ static void parse_readme(const char *readme, char **filename, char **ref, struct } if (!(*ref) && readme[0] != '/') - *filename = cgit_fmtalloc("%s/%s", repo->path, readme); + *filename = xstrfmt("%s/%s", repo->path, readme); else *filename = xstrdup(readme); } @@ -1072,7 +1015,7 @@ static void choose_readme(struct cgit_repo *repo) continue; } if (ref) { - if (cgit_ref_path_exists(filename, ref, 1)) { + if (cgit_ref_path_exists(filename, ref)) { found = 1; break; } @@ -1090,7 +1033,32 @@ static void choose_readme(struct cgit_repo *repo) string_list_append(&repo->readme, filename)->util = ref; } -static void prepare_repo_env(int *nongit, int *err) +/* + * Export the CGIT_REPO_* variables that filters and hooks read. + */ +static void prepare_repo_env(struct cgit_repo *repo) +{ + const struct { + const char *name; + const char *value; + } vars[] = { + { .name = "CGIT_REPO_URL", .value = repo->url }, + { .name = "CGIT_REPO_NAME", .value = repo->name }, + { .name = "CGIT_REPO_PATH", .value = repo->path }, + { .name = "CGIT_REPO_OWNER", .value = repo->owner }, + { .name = "CGIT_REPO_DEFBRANCH", .value = repo->defbranch }, + { .name = "CGIT_REPO_SECTION", .value = repo->section }, + { .name = "CGIT_REPO_CLONE_URL", .value = repo->clone_url } + }; + size_t i; + + for (i = 0; i < ARRAY_SIZE(vars); i++) + if (vars[i].value && setenv(vars[i].name, vars[i].value, 1)) + fprintf(stderr, "[cgit] Unable to set %s=%s: %s (%d)\n", + vars[i].name, vars[i].value, strerror(errno), errno); +} + +static void open_repo(int *nongit, int *err) { // A repository configured without a path has nothing to open. if (!ctx.repo->path) { @@ -1104,14 +1072,13 @@ static void prepare_repo_env(int *nongit, int *err) setup_git_directory_gently(the_repository, nongit); *err = errno; // Notes come out of the object store, which a repository that failed - // to open has none of, and git dies loading a notes ref that names - // no tree, so such a ref is passed over. + // to open has none of. The commit and log pages always ask for them, + // so they are set up either way, but a default ref that names no tree + // is left out, since git dies loading one. if (!*nongit) { struct display_notes_opt opt; char *ref = default_notes_ref(the_repository); - // The notes are always set up, since the commit and log pages - // ask for them, only with no ref when the default names no tree. init_display_notes(&opt); opt.use_default_notes = cgit_notes_ref_usable(ref); load_display_notes(&opt); @@ -1126,11 +1093,16 @@ static void prepare_repo_env(int *nongit, int *err) */ static int prepare_repo_cmd(int nongit, int err, int clone) { + // Static, since the pages replace the title without freeing it. Reset + // on entry, since a failed cache fill renders the page a second time. + static struct strbuf title = STRBUF_INIT; struct object_id oid; + strbuf_reset(&title); if (nongit) { const char *name = ctx.repo->name; - ctx.page.title = cgit_fmtalloc("%s - %s", ctx.cfg.root_title, "config error"); + strbuf_addf(&title, "%s - %s", ctx.cfg.root_title, "config error"); + ctx.page.title = title.buf; ctx.repo = NULL; cgit_print_error_page( 404, "Not Found", "Failed to open %s: %s", name, @@ -1138,7 +1110,8 @@ static int prepare_repo_cmd(int nongit, int err, int clone) ); return 1; } - ctx.page.title = cgit_fmtalloc("%s - %s", ctx.repo->name, ctx.cfg.root_title); + strbuf_addf(&title, "%s - %s", ctx.repo->name, ctx.cfg.root_title); + ctx.page.title = title.buf; if (!ctx.repo->defbranch) ctx.repo->defbranch = guess_defbranch(); @@ -1152,19 +1125,17 @@ static int prepare_repo_cmd(int nongit, int err, int clone) // The dumb transport serves refs and objects off the disk and // asks nothing of the head, and an empty repository clones. if (clone) { - cgit_prepare_repo_env(ctx.repo); + prepare_repo_env(ctx.repo); return 0; } ctx.empty_repo = 1; // Before the document starts, since the <head> carries // <link rel='vcs-git'> and those clone urls expand macros such // as $CGIT_REPO_URL out of this environment. - cgit_prepare_repo_env(ctx.repo); - cgit_print_http_headers(); - cgit_print_docstart(); - cgit_print_pageheader(); + prepare_repo_env(ctx.repo); + cgit_print_layout_start(); cgit_print_empty_repo(); - cgit_print_docend(); + cgit_print_layout_end(); return 1; } @@ -1185,7 +1156,7 @@ static int prepare_repo_cmd(int nongit, int err, int clone) return 1; } string_list_sort(&ctx.repo->submodules); - cgit_prepare_repo_env(ctx.repo); + prepare_repo_env(ctx.repo); choose_readme(ctx.repo); return 0; } @@ -1199,17 +1170,15 @@ static void process_request(void) // whatever page it asked for. if (!ctx.env.authenticated) { ctx.page.title = "Authentication Required"; - cgit_print_http_headers(); - cgit_print_docstart(); - cgit_print_pageheader(); + cgit_print_layout_start(); open_auth_filter("body"); cgit_close_filter(ctx.cfg.auth_filter); - cgit_print_docend(); + cgit_print_layout_end(); return; } if (ctx.repo) - prepare_repo_env(&nongit, &err); + open_repo(&nongit, &err); cmd = cgit_get_cmd(); if (!cmd || (!ctx.cfg.enable_http_clone && cmd->is_clone)) { @@ -1340,18 +1309,12 @@ int cmd_main(int argc, const char **argv) set_die_routine(die_routine); prepare_context(); - cgit_repolist.length = 0; - cgit_repolist.count = 0; - cgit_repolist.repos = NULL; parse_args(argc, argv); config_file_parse(cgit_expand_macros(ctx.env.cgit_config), apply_config); ctx.repo = NULL; http_parse_querystring(ctx.qry.raw, apply_query_param); - if (!ctx.cfg.virtual_root && ctx.cfg.script_name) - ctx.cfg.virtual_root = cgit_ensure_end(ctx.cfg.script_name, '/'); - // Falling back to PATH_INFO lets cgit serve virtual urls without a // rewrite rule in the web server, and folding it into the raw query // string keeps it part of the cache key. @@ -1362,7 +1325,7 @@ int cmd_main(int argc, const char **argv) path++; ctx.qry.url = xstrdup(path); if (ctx.qry.raw) { - char *path_and_query = cgit_fmtalloc("%s?%s", path, ctx.qry.raw); + char *path_and_query = xstrfmt("%s?%s", path, ctx.qry.raw); free(ctx.qry.raw); ctx.qry.raw = path_and_query; } else { @@ -1387,7 +1350,6 @@ int cmd_main(int argc, const char **argv) max_bytes, process_request); strbuf_release(&cache_key); - cgit_cleanup_filters(); // A slot that could not be sent because the client went away is not // worth an error page nobody will read. if (err && err != EPIPE) |
