/* * The helpers that no single page owns, along with the process wide state * every page reads. Repositories are registered here as cgitrc is parsed, the * reference and commit records that the renderers list are built and freed * here, 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 "parsing.h" #include "shared.h" #define MACRO_EXPANSION_BUFSIZE (1024 * 8) // The number of context lines git itself defaults to. #define DEFAULT_DIFF_CONTEXT 3 typedef struct { const char *name; const char *value; } env_var; struct cgit_repolist cgit_repolist; struct cgit_context ctx; char *cgit_default_repo_desc = "[no description]"; // A diff line that arrived in pieces and is still waiting for its end, see // emit_line. static char *fragment; static int fragment_len; static void free_refinfo(struct refinfo *ref) { free((char *)ref->refname); switch (ref->object->type) { case OBJ_TAG: cgit_free_taginfo(ref->tag); break; case OBJ_COMMIT: cgit_free_commitinfo(ref->commit); break; } free(ref); } static struct refinfo *make_refinfo(const char *refname, const struct object_id *oid) { struct refinfo *ref; ref = xcalloc(1, sizeof(struct refinfo)); ref->refname = xstrdup(refname); ref->object = parse_object(the_repository, oid); if (!ref->object) { free((char *)ref->refname); free(ref); return NULL; } switch (ref->object->type) { case OBJ_TAG: ref->tag = cgit_parse_tag((struct tag *)ref->object); break; case OBJ_COMMIT: ref->commit = cgit_parse_commit((struct commit *)ref->object); break; } return ref; } static int load_mmfile(mmfile_t *file, const struct object_id *oid) { enum object_type type; // 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, (unsigned long *)&file->size); // odb_read_object leaves size untouched when it fails, so the caller // has to be told rather than handed a buffer with an unset length. return file->ptr != NULL; } /* * The test is on the oid rather than on the size, because load_mmfile uses a * literal only for a null oid, and a real blob that happens to be empty does * own its buffer. */ static void release_mmfile(mmfile_t *file, const struct object_id *oid) { if (!is_null_oid(oid)) free(file->ptr); } /* * Xdiff emits buffers that need not end on a line boundary, so a trailing * fragment is held back and joined with whatever arrives next. Git's own * xdiff_outf keeps that fragment in a callback struct, which is not an option * here because priv already carries the caller's function. */ static int emit_line(void *priv, mmbuffer_t *mb, int nbuf) { linediff_fn emit = (linediff_fn)priv; int i; for (i = 0; i < nbuf; i++) { if (mb[i].ptr[mb[i].size-1] != '\n') { fragment = xrealloc(fragment, fragment_len + mb[i].size); memcpy(fragment + fragment_len, mb[i].ptr, mb[i].size); fragment_len += mb[i].size; continue; } if (!fragment) { emit(mb[i].ptr, mb[i].size); continue; } fragment = xrealloc(fragment, fragment_len + mb[i].size); memcpy(fragment + fragment_len, mb[i].ptr, mb[i].size); emit(fragment, fragment_len + mb[i].size); free(fragment); fragment = NULL; fragment_len = 0; } if (fragment) { emit(fragment, fragment_len); free(fragment); fragment = NULL; fragment_len = 0; } return 0; } // Takes an unsigned char because a byte over 0x7f is negative in a plain // char wherever char is signed, and a negative one is not a value the ctype // tests are 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; } int cgit_die_unless_zero(int result, const char *msg) { if (result != 0) die_errno("%s", msg); return result; } int cgit_die_unless_positive(int result, const char *msg) { if (result <= 0) die_errno("%s", msg); return result; } int cgit_die_unless_non_negative(int result, const char *msg) { if (result < 0) die_errno("%s", msg); return result; } struct cgit_repo *cgit_add_repo(const char *url) { struct cgit_repo *repo; if (++cgit_repolist.count > cgit_repolist.length) { if (cgit_repolist.length == 0) cgit_repolist.length = 8; else cgit_repolist.length *= 2; cgit_repolist.repos = xrealloc(cgit_repolist.repos, cgit_repolist.length * sizeof(struct cgit_repo)); } repo = &cgit_repolist.repos[cgit_repolist.count-1]; memset(repo, 0, sizeof(struct cgit_repo)); repo->url = cgit_trim_end(url, '/'); if (repo->url) *strchrnul(repo->url, '\n') = '\0'; repo->name = repo->url; repo->path = NULL; repo->desc = cgit_default_repo_desc; repo->extra_head_content = NULL; repo->owner = NULL; 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_log_filecount = ctx.cfg.enable_log_filecount; repo->enable_log_linecount = ctx.cfg.enable_log_linecount; repo->enable_remote_branches = ctx.cfg.enable_remote_branches; repo->enable_subject_links = ctx.cfg.enable_subject_links; repo->enable_html_serving = ctx.cfg.enable_html_serving; repo->enable_stats = ctx.cfg.enable_stats; 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->readme = ctx.cfg.readme; 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->clone_url = ctx.cfg.clone_url; repo->submodules.strdup_strings = 1; repo->hide = repo->ignore = 0; 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_free_commitinfo(struct commitinfo *info) { free(info->author); free(info->author_email); free(info->committer); free(info->committer_email); free(info->subject); free(info->msg); free(info->msg_encoding); free(info); } char *cgit_trim_end(const char *str, char c) { size_t len; if (str == NULL) return NULL; len = strlen(str); while (len > 0 && str[len - 1] == c) len--; if (len == 0) return NULL; return xstrndup(str, len); } char *cgit_ensure_end(const char *str, char c) { size_t len = strlen(str); char *result; if (len && str[len - 1] == c) return xstrndup(str, len); result = xmalloc(len + 2); memcpy(result, str, len); result[len] = '/'; result[len + 1] = '\0'; return result; } void strbuf_ensure_end(struct strbuf *sb, char c) { if (!sb->len || sb->buf[sb->len - 1] != c) strbuf_addch(sb, c); } static void add_ref(struct reflist *list, struct refinfo *ref) { size_t size; if (list->count >= list->alloc) { list->alloc += (list->alloc ? list->alloc : 4); size = list->alloc * sizeof(struct refinfo *); list->refs = xrealloc(list->refs, size); } list->refs[list->count++] = ref; } void cgit_free_taginfo(struct taginfo *info) { // NULL is accepted because cgit_parse_tag returns it for a tag object // that cannot be read, and a ref left holding that still gets freed // along with the rest of its list. if (!info) return; free(info->tagger); free(info->tagger_email); free(info->msg); free(info); } void cgit_free_reflist_inner(struct reflist *list) { int i; for (i = 0; i < list->count; i++) free_refinfo(list->refs[i]); free(list->refs); } int cgit_refs_cb(const struct reference *ref, void *cb_data) { struct reflist *list = (struct reflist *)cb_data; struct refinfo *info = make_refinfo(ref->name, ref->oid); if (info) add_ref(list, info); return 0; } void cgit_diff_tree_cb(struct diff_queue_struct *q, struct diff_options *options, 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]); } } int 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) { mmfile_t old_file, new_file; xpparam_t diff_params; xdemitconf_t emit_params; xdemitcb_t emit_cb; unsigned long 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 (ctx.cfg.max_blob_size && (old_bytes / 1024 > (unsigned long)ctx.cfg.max_blob_size || new_bytes / 1024 > (unsigned long)ctx.cfg.max_blob_size)) { *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)); diff_params.flags = XDF_NEED_MINIMAL; 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_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; struct pathspec_item *item; 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) { item = xcalloc(1, sizeof(*item)); item->match = xstrdup(prefix); item->len = strlen(prefix); opt.pathspec.nr = 1; opt.pathspec.items = item; } 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); } /* * Git's parse_date_format dies on anything it does not recognize, which would * turn a typo in cgitrc into a failed request, so only the formats cgit * documents reach it. */ void cgit_parse_date_format(const char *format, struct date_mode *mode) { static const char * const names[] = { "default", "human", "iso", "iso-strict", "iso8601", "iso8601-strict", "local", "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; } 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; } } void cgit_prepare_repo_env(struct cgit_repo *repo) { const env_var 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 warning: failed to set env: %s=%s\n", vars[i].name, vars[i].value); } int cgit_read_first_line(const char *path, char **buf, size_t *size) { int fd, err; ssize_t got; 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; } *buf = xmalloc(st.st_size + 1); got = read_in_full(fd, *buf, st.st_size); err = errno; if (got < 0) { free(*buf); *buf = NULL; *size = 0; close(fd); return err; } *size = got; (*buf)[*size] = '\0'; *strchrnul(*buf, '\n') = '\0'; close(fd); return (*size == (size_t)st.st_size ? 0 : err); } char *cgit_strdup_first_line(const char *text) { char *line = xstrdup(text); *strchrnul(line, '\n') = '\0'; return line; } 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; } start = NULL; // Step back so the character that ended the // token is written again past the expansion, // where it may open a token of its own. text--; } 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; } char *cgit_get_mimetype_for_filename(const char *filename) { const char *ext; char *type, line[1024]; struct string_list fields = STRING_LIST_INIT_NODUP; size_t i; FILE *file; struct string_list_item *entry; if (!filename) return NULL; ext = strrchr(filename, '.'); if (!ext) return NULL; ++ext; if (!ext[0]) return NULL; entry = string_list_lookup(&ctx.cfg.mimetypes, ext); if (entry) return xstrdup(entry->util); if (!ctx.cfg.mimetype_file) return NULL; file = fopen(ctx.cfg.mimetype_file, "r"); if (!file) return NULL; while (fgets(line, sizeof(line), file)) { if (!line[0] || line[0] == '#') continue; // A line of a mime.types file is one type followed by every // extension that maps to it. string_list_split_in_place(&fields, line, " \t\r\n", -1); string_list_remove_empty_items(&fields, 0); type = fields.items[0].string; for (i = 1; i < fields.nr; i++) { if (!strcasecmp(ext, fields.items[i].string)) { fclose(file); return xstrdup(type); } } string_list_clear(&fields, 0); } fclose(file); return NULL; }