/* * The helpers that no single page owns, along with the process-wide state * every page reads. Repositories are registered here as cgitrc is parsed, and * git's diff machinery is wrapped so a caller is handed whole filepairs or * whole lines instead of xdiff buffers. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" #include "shared.h" #define MACRO_EXPANSION_BUFSIZE (1024 * 8) #define MAX_FIRST_LINE_READ (1024 * 64) struct cgit_repolist cgit_repolist; struct cgit_context ctx; char cgit_default_repo_desc[] = "[no description]"; struct cgit_repo *cgit_add_repo(const char *url) { struct cgit_repo *repo; ALLOC_GROW(cgit_repolist.repos, cgit_repolist.count + 1, cgit_repolist.length); repo = &cgit_repolist.repos[cgit_repolist.count++]; memset(repo, 0, sizeof(struct cgit_repo)); repo->url = cgit_trim_end(url, '/'); if (repo->url) { *strchrnul(repo->url, '\n') = '\0'; } else { // Nothing can address a repository with no url, and every lookup // compares the url, so it is kept out of the way. fprintf(stderr, "[cgit] Ignoring repository with an empty url\n"); repo->url = xstrdup(""); repo->ignore = 1; } repo->name = repo->url; repo->desc = cgit_default_repo_desc; repo->section = ctx.cfg.section; repo->snapshots = ctx.cfg.snapshots; repo->enable_blame = ctx.cfg.enable_blame; repo->enable_commit_graph = ctx.cfg.enable_commit_graph; repo->enable_follow_links = ctx.cfg.enable_follow_links; repo->enable_gitmodules_links = ctx.cfg.enable_gitmodules_links; repo->enable_log_filecount = ctx.cfg.enable_log_filecount; repo->enable_log_linecount = ctx.cfg.enable_log_linecount; repo->enable_mailmap = ctx.cfg.enable_mailmap; repo->enable_remote_branches = ctx.cfg.enable_remote_branches; repo->enable_subject_links = ctx.cfg.enable_subject_links; repo->enable_commit_trailers = ctx.cfg.enable_commit_trailers; repo->enable_html_serving = ctx.cfg.enable_html_serving; repo->max_stats = ctx.cfg.max_stats; repo->branch_sort = ctx.cfg.branch_sort; repo->commit_sort = ctx.cfg.commit_sort; repo->module_link = ctx.cfg.module_link; repo->mtime = -1; repo->about_filter = ctx.cfg.about_filter; repo->commit_filter = ctx.cfg.commit_filter; repo->source_filter = ctx.cfg.source_filter; repo->email_filter = ctx.cfg.email_filter; repo->trailer_filter = ctx.cfg.trailer_filter; repo->clone_url = ctx.cfg.clone_url; repo->submodules.strdup_strings = 1; return repo; } struct cgit_repo *cgit_get_repoinfo(const char *url) { int i; struct cgit_repo *repo; for (i = 0; i < cgit_repolist.count; i++) { repo = &cgit_repolist.repos[i]; if (repo->ignore) continue; if (!strcmp(repo->url, url)) return repo; } return NULL; } void cgit_diff_tree_cb(struct diff_queue_struct *q, struct diff_options *options UNUSED, void *data) { filepair_fn fn = *(filepair_fn *)data; int i; for (i = 0; i < q->nr; i++) { // An unmerged path has no single pair of blobs to show. if (q->queue[i]->status == 'U') continue; fn(q->queue[i]); } } static int load_mmfile(mmfile_t *file, const struct object_id *oid) { enum object_type type; size_t size; // A null oid is the absent side of an add or a delete, and diffs as an // empty file rather than as a failure. if (is_null_oid(oid)) { file->ptr = (char *)""; file->size = 0; return 1; } file->ptr = odb_read_object(the_repository->objects, oid, &type, &size); if (!file->ptr) return 0; file->size = size; return 1; } /* * The test is on the oid rather than the size, because load_mmfile uses a * literal only for a null oid, while a real but empty blob owns its buffer. */ static void release_mmfile(mmfile_t *file, const struct object_id *oid) { if (!is_null_oid(oid)) free(file->ptr); } /* * Xdiff hands over one line per call, in pieces that are the prefix, the * record and, for a record with no newline, the marker saying so. They are * joined here before the line reaches fn. */ static int emit_line(void *priv, mmbuffer_t *mb, int nbuf) { linediff_fn emit = *(linediff_fn *)priv; struct strbuf line = STRBUF_INIT; int i; for (i = 0; i < nbuf; i++) strbuf_add(&line, mb[i].ptr, mb[i].size); emit(line.buf, line.len); strbuf_release(&line); return 0; } int cgit_diff_files(const struct object_id *old_oid, const struct object_id *new_oid, size_t *old_size, size_t *new_size, int *binary, int context, int ignorews, linediff_fn fn) { mmfile_t old_file, new_file; xpparam_t diff_params; xdemitconf_t emit_params; xdemitcb_t emit_cb; size_t old_bytes = 0, new_bytes = 0; // Read the object headers first so an oversized blob is never inflated // into memory just to be diffed. Reporting it as binary suppresses // inlining the same way max-blob-size does in the other views. if ( !is_null_oid(old_oid) && odb_read_object_info(the_repository->objects, old_oid, &old_bytes) < 0 ) return 1; if ( !is_null_oid(new_oid) && odb_read_object_info(the_repository->objects, new_oid, &new_bytes) < 0 ) return 1; *old_size = old_bytes; *new_size = new_bytes; if (cgit_blob_too_large(old_bytes) || cgit_blob_too_large(new_bytes)) { *binary = 1; return 0; } if (!load_mmfile(&old_file, old_oid)) return 1; if (!load_mmfile(&new_file, new_oid)) { release_mmfile(&old_file, old_oid); return 1; } if ( buffer_is_binary(old_file.ptr, old_file.size) || buffer_is_binary(new_file.ptr, new_file.size) ) { *binary = 1; release_mmfile(&old_file, old_oid); release_mmfile(&new_file, new_oid); return 0; } memset(&diff_params, 0, sizeof(diff_params)); memset(&emit_params, 0, sizeof(emit_params)); memset(&emit_cb, 0, sizeof(emit_cb)); // Without XDF_NEED_MINIMAL, as git diffs by default: on a file whose // lines were reordered, a minimal diff costs time quadratic in its // length. if (ignorews) diff_params.flags |= XDF_IGNORE_WHITESPACE; emit_params.ctxlen = context > 0 ? context : DEFAULT_DIFF_CONTEXT; emit_params.flags = XDL_EMIT_FUNCNAMES; emit_cb.out_line = emit_line; emit_cb.priv = &fn; xdl_diff(&old_file, &new_file, &diff_params, &emit_params, &emit_cb); release_mmfile(&old_file, old_oid); release_mmfile(&new_file, new_oid); return 0; } void cgit_diff_limit_path(struct diff_options *opt, const char *prefix) { cgit_literal_pathspec(&opt->pathspec, xmalloc(sizeof(struct pathspec_item)), xstrdup(prefix)); } void cgit_diff_tree(const struct object_id *old_oid, const struct object_id *new_oid, filepair_fn fn, const char *prefix, int ignorews) { struct diff_options opt; repo_diff_setup(the_repository, &opt); opt.output_format = DIFF_FORMAT_CALLBACK; opt.detect_rename = 1; opt.rename_limit = ctx.cfg.renamelimit; opt.flags.recursive = 1; if (ignorews) DIFF_XDL_SET(&opt, IGNORE_WHITESPACE); opt.format_callback = cgit_diff_tree_cb; opt.format_callback_data = &fn; if (prefix) cgit_diff_limit_path(&opt, prefix); diff_setup_done(&opt); if (old_oid && !is_null_oid(old_oid)) diff_tree_oid(old_oid, new_oid, "", &opt); else diff_root_tree_oid(new_oid, "", &opt); diffcore_std(&opt); diff_flush(&opt); } void cgit_diff_commit(struct commit *commit, filepair_fn fn, const char *prefix) { const struct object_id *old_oid = NULL; if (commit->parents) old_oid = &commit->parents->item->object.oid; cgit_diff_tree(old_oid, &commit->object.oid, fn, prefix, ctx.qry.ignorews); } int cgit_blob_too_large(size_t size) { return ctx.cfg.max_blob_size && size / 1024 > (size_t)ctx.cfg.max_blob_size; } void cgit_literal_pathspec(struct pathspec *ps, struct pathspec_item *item, const char *path) { int len = path ? strlen(path) : 0; // nowildcard_len matching len makes git treat the path as literal // rather than as a glob. *item = (struct pathspec_item){ .match = path, .len = len, .nowildcard_len = len }; ps->nr = path ? 1 : 0; ps->items = item; } char *cgit_trim_end(const char *str, char c) { size_t len; if (!str) return NULL; len = strlen(str); while (len > 0 && str[len - 1] == c) len--; if (len == 0) return NULL; return xstrndup(str, len); } int cgit_read_first_line(const char *path, char **buf) { int fd, err; ssize_t got; size_t want; struct stat st; fd = open(path, O_RDONLY); if (fd == -1) return errno; if (fstat(fd, &st)) { err = errno; close(fd); return err; } if (!S_ISREG(st.st_mode)) { close(fd); return EISDIR; } // Only the first line is wanted, and a description file in a scanned // repository is whatever a pusher wrote, so the read is bounded. want = st.st_size < MAX_FIRST_LINE_READ ? st.st_size : MAX_FIRST_LINE_READ; *buf = xmalloc(want + 1); got = read_in_full(fd, *buf, want); if (got < 0) { err = errno; free(*buf); *buf = NULL; close(fd); return err; } (*buf)[got] = '\0'; *strchrnul(*buf, '\n') = '\0'; close(fd); return 0; } char *cgit_strdup_first_line(const char *text) { char *line = xstrdup(text); *strchrnul(line, '\n') = '\0'; return line; } /* * The lead-byte ranges fold in the overlong, surrogate and out-of-range * cases, so 0 is the only error signal a caller needs. */ size_t cgit_utf8_seq_len(const unsigned char *p, size_t left) { size_t len, i; if (p[0] >= 0xc2 && p[0] <= 0xdf) len = 2; else if (p[0] >= 0xe0 && p[0] <= 0xef) len = 3; else if (p[0] >= 0xf0 && p[0] <= 0xf4) len = 4; else return 0; if (left < len) return 0; for (i = 1; i < len; i++) if ((p[i] & 0xc0) != 0x80) return 0; if (p[0] == 0xe0 && p[1] < 0xa0) return 0; if (p[0] == 0xed && p[1] > 0x9f) return 0; if (p[0] == 0xf0 && p[1] < 0x90) return 0; if (p[0] == 0xf4 && p[1] > 0x8f) return 0; return len; } void cgit_utf8_sanitize(char *text) { unsigned char *p = (unsigned char *)text; size_t left = text ? strlen(text) : 0; while (left) { size_t seq = *p < 0x80 ? 1 : cgit_utf8_seq_len(p, left); if (!seq) { *p = '?'; seq = 1; } p += seq; left -= seq; } } /* * Takes an unsigned char because a byte over 0x7f is negative where char is * signed, which the ctype tests are not defined for. */ static int is_token_char(unsigned char c) { return isalnum(c) || c == '_'; } /* * Replace the variable name held at name with its value from the environment, * writing at most room bytes including the terminator, and return where the * text now ends. */ static char *expand_macro(char *name, size_t room) { char *value; size_t len; len = 0; value = getenv(name); if (value) { len = strlen(value) + 1; if (len > room) len = room; strlcpy(name, value, len); --len; } return name + len; } char *cgit_expand_macros(const char *text) { static char result[MACRO_EXPANSION_BUFSIZE]; char *limit = result + MACRO_EXPANSION_BUFSIZE - 1; char *out, *start; out = result; start = NULL; while (out < limit && text && *text) { *out = *text; if (start) { if (!is_token_char(*text)) { if (out - start > 0) { *out = '\0'; out = expand_macro(start, limit - start) - 1; // Step back so the byte that ended the // token is written again and may open a // new one. A bare dollar expanded nothing // and its follower is already in place. text--; } start = NULL; } out++; text++; continue; } if (*text == '$') { start = out; text++; continue; } out++; text++; } *out = '\0'; if (start && out - start > 0) { out = expand_macro(start, limit - start); *out = '\0'; } return result; } int cgit_readme_is_tracked(const char *readme) { const char *colon = strchr(readme, ':'); return colon && colon[1]; } /* * Git's wider syntax is refused, since :/pattern and rev^{/pattern} walk the * whole history for a match, and a leading dash reads as an option further * down. */ int cgit_valid_rev(const char *rev) { const char *p = rev; if (!rev || !*rev || *rev == '-') return 0; while (isxdigit((unsigned char)*p)) p++; if (!*p) return 1; return !check_refname_format(rev, REFNAME_ALLOW_ONELEVEL); } int cgit_notes_ref_usable(const char *notes_ref) { struct object_id oid; if (repo_get_oid_treeish(the_repository, notes_ref, &oid)) return 0; return !!parse_tree_indirect(&oid); }