Diff options
Diffstat (limited to '')
44 files changed, 1640 insertions, 2583 deletions
diff --git a/source/cache.c b/source/cache.c index 9594fd6..0a3166d 100644 --- a/source/cache.c +++ b/source/cache.c @@ -12,7 +12,6 @@ #include "cache.h" #include "cgit.h" #include "html.h" -#include "shared.h" #include <sys/file.h> #ifdef HAVE_LINUX_SENDFILE #include <sys/sendfile.h> @@ -31,21 +30,6 @@ #define FNV_OFFSET 0x811c9dc5 #define FNV_PRIME 0x01000193 -/* - * Cache trouble goes to stderr, which under CGI is the web server's error - * log, so that it cannot land in the middle of the page being written to - * stdout. - */ -__attribute__((format (printf,1,2))) -static void log_error(const char *format, ...) -{ - va_list args; - - va_start(args, format); - vfprintf(stderr, format, args); - va_end(args); -} - struct cache_slot { const char *key; size_t keylen; @@ -112,17 +96,12 @@ static int key_fits_slot(const char *key) return strlen(key) + 1 <= CACHE_BUFSIZE; } -static int close_slot(struct cache_slot *slot) +static void close_fd(int *fd) { - int err = 0; - - if (slot->cache_fd > 0) { - if (close(slot->cache_fd)) - err = errno; - else - slot->cache_fd = -1; + if (*fd > 0) { + close(*fd); + *fd = -1; } - return err; } static int print_slot(struct cache_slot *slot) @@ -182,7 +161,7 @@ static int serve_slot(struct cache_slot *slot) err = print_slot(slot); // A client that went away mid-page is not trouble worth a line. if (err && err != EPIPE) - log_error("[cgit] Unable to send slot %s: %s (%d)\n", slot->path, strerror(err), err); + fprintf(stderr, "[cgit] Unable to send slot %s: %s (%d)\n", slot->path, strerror(err), err); return err; } @@ -212,63 +191,95 @@ static int is_modified(struct cache_slot *slot) ); } -static int close_lock(struct cache_slot *slot) -{ - int err = 0; - - if (slot->lock_fd > 0) { - if (close(slot->lock_fd)) - err = errno; - else - slot->lock_fd = -1; - } - return err; -} - /* * The lock file becomes the slot once it is renamed, so it has to open with * the key the same way a slot does. The lock is taken without blocking, * because failing to get it is how a second process learns that this slot is - * already being rebuilt, so it returns an errno instead of waiting. + * already being rebuilt, so it returns an errno instead of waiting. Every + * other failure is logged here, for both callers. */ -static int lock_slot(struct cache_slot *slot) +int cache_lock_file(const char *path, int *fd) { struct stat held, named; + int err; - slot->lock_fd = open(slot->lock_path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); - if (slot->lock_fd == -1) - return errno; - // An flock rather than a POSIX record lock. The descriptor is put on - // stdout for the fill, and a record lock is dropped as soon as any + // Close-on-exec, so that a program a cache fill runs gets the file + // only as its stdout, and holds the lock no longer than it can write + // there. + *fd = open(path, O_RDWR | O_CREAT | O_CLOEXEC, S_IRUSR | S_IWUSR); + if (*fd == -1) { + err = errno; + fprintf(stderr, "[cgit] Unable to open %s: %s (%d)\n", path, strerror(err), err); + return err; + } + // An flock rather than a POSIX record lock. A slot's descriptor is put + // on stdout for the fill, and a record lock is dropped as soon as any // descriptor on the file closes, which an exec filter does to stdout, - // while an flock holds until the last descriptor closes. - if (flock(slot->lock_fd, LOCK_EX | LOCK_NB) < 0) { - int saved_errno = errno; - close(slot->lock_fd); - slot->lock_fd = -1; - return saved_errno; + // while an flock holds until the last descriptor closes. Either way a + // holder that dies takes its lock with it. + if (flock(*fd, LOCK_EX | LOCK_NB) < 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, keeps the cache from + // ever being written and does deserve one. + err = errno; + if (err != EWOULDBLOCK) + fprintf(stderr, "[cgit] Unable to lock %s: %s (%d)\n", path, strerror(err), err); + goto fail; } // The lock landed on whatever inode the path named at open. A holder - // finishing in between renames that inode into place as the live - // slot, and once the path is confirmed to still name this file that - // rename can no longer happen, because it takes the lock held here. + // finishing in between renames that inode into place, 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(slot->lock_fd, &held) || - stat(slot->lock_path, &named) || - held.st_ino != named.st_ino || + fstat(*fd, &held) || + stat(path, &named) || + held.st_ino != named.st_ino || held.st_dev != named.st_dev ) { - close(slot->lock_fd); - slot->lock_fd = -1; - return EAGAIN; + err = EAGAIN; + goto fail; } // 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(slot->lock_fd, 0) < 0) - return errno; - if (write_in_full(slot->lock_fd, slot->key, slot->keylen + 1) < 0) - return errno; + if (ftruncate(*fd, 0) < 0) { + err = errno; + fprintf(stderr, "[cgit] Unable to write %s: %s (%d)\n", path, strerror(err), err); + unlink(path); + goto fail; + } return 0; + +fail: + close(*fd); + *fd = -1; + return err; +} + +static int lock_slot(struct cache_slot *slot) +{ + int err = cache_lock_file(slot->lock_path, &slot->lock_fd); + + if (err) + return err; + if (write_in_full(slot->lock_fd, slot->key, slot->keylen + 1) < 0) { + err = errno; + fprintf(stderr, "[cgit] Unable to write %s: %s (%d)\n", slot->lock_path, strerror(err), err); + unlink(slot->lock_path); + close(slot->lock_fd); + slot->lock_fd = -1; + return err; + } + return 0; +} + +static void restore_stdout(struct cache_slot *slot) +{ + if (slot->saved_stdout >= 0) { + dup2(slot->saved_stdout, STDOUT_FILENO); + close(slot->saved_stdout); + slot->saved_stdout = -1; + } } static int unlock_slot(struct cache_slot *slot, int replace_old_slot) @@ -280,11 +291,7 @@ static int unlock_slot(struct cache_slot *slot, int replace_old_slot) else err = unlink(slot->lock_path); - if (slot->saved_stdout >= 0) { - dup2(slot->saved_stdout, STDOUT_FILENO); - close(slot->saved_stdout); - slot->saved_stdout = -1; - } + restore_stdout(slot); if (err) return errno; @@ -328,11 +335,7 @@ int cache_abandon_fill(void) // caller that could not write it must not try again here. fflush(stdout); - if (slot->saved_stdout >= 0) { - dup2(slot->saved_stdout, STDOUT_FILENO); - close(slot->saved_stdout); - slot->saved_stdout = -1; - } + restore_stdout(slot); replay_lock_file(slot); unlink(slot->lock_path); return 1; @@ -347,7 +350,7 @@ static void publish_slot(struct cache_slot *slot) int err = unlock_slot(slot, 1); if (err) - log_error("[cgit] Unable to publish cache slot %s: %s (%d)\n", slot->path, + fprintf(stderr, "[cgit] Unable to publish cache slot %s: %s (%d)\n", slot->path, strerror(err), err); } @@ -405,13 +408,13 @@ static void refresh_slot(struct cache_slot *slot) // lock_slot, the copy already open is served rather than the newer. if (is_modified(slot) || fill_slot(slot)) { unlock_slot(slot, 0); - close_lock(slot); + close_fd(&slot->lock_fd); } else if (slot->abandoned) { // The abandoned fill answered the visitor itself and removed // the lock file, so only the descriptor is left to clean up. - close_lock(slot); + close_fd(&slot->lock_fd); } else { - close_slot(slot); + close_fd(&slot->cache_fd); if (slot->oversized) { // The expired copy goes too, or every later request // would refill it and drop the result again. @@ -436,11 +439,11 @@ static int process_slot(struct cache_slot *slot) // visitor, and serving the stale copy still open would append // a second page to that answer. if (slot->abandoned) { - close_slot(slot); + close_fd(&slot->cache_fd); return 0; } err = serve_slot(slot); - close_slot(slot); + close_fd(&slot->cache_fd); return err; } @@ -448,23 +451,22 @@ static int process_slot(struct cache_slot *slot) // and two popular pages sharing one slot evict each other on every // alternating visit. if (!err) - log_error("[cgit] Cache slot %s holds a different key, consider a larger cache-size\n", + fprintf(stderr, "[cgit] Cache slot %s holds a different key, consider a larger cache-size\n", slot->path); // If any part of creating a slot fails the page is still rendered // straight to the client and the caller is told the request succeeded, // because it did. - close_slot(slot); - if ((err = lock_slot(slot)) != 0) { - log_error("[cgit] Unable to lock slot %s: %s (%d)\n", slot->lock_path, strerror(err), err); + close_fd(&slot->cache_fd); + if (lock_slot(slot)) { slot->fn(); return 0; } if ((err = fill_slot(slot)) != 0) { - log_error("[cgit] Unable to fill slot %s: %s (%d)\n", slot->lock_path, strerror(err), err); + fprintf(stderr, "[cgit] Unable to fill slot %s: %s (%d)\n", slot->lock_path, strerror(err), err); unlock_slot(slot, 0); - close_lock(slot); + close_fd(&slot->lock_fd); // Rendering again is only right when nothing was delivered, // and an abandoned fill has already sent the error page. if (!slot->abandoned) @@ -473,7 +475,7 @@ static int process_slot(struct cache_slot *slot) } if (slot->abandoned) { - close_lock(slot); + close_fd(&slot->lock_fd); return 0; } @@ -486,26 +488,11 @@ static int process_slot(struct cache_slot *slot) else publish_slot(slot); err = serve_slot(slot); - close_slot(slot); + close_fd(&slot->cache_fd); return err; } /* - * The result lives in a static buffer and is only good until the next call. - */ -static const char *format_time(const char *format, time_t when) -{ - static char buf[64]; - struct tm tm; - - if (!when) - return "-"; - gmtime_r(&when, &tm); - strftime(buf, sizeof(buf) - 1, format, &tm); - return buf; -} - -/* * The accumulator is unsigned long, so on a 64 bit host this is not the * published FNV-1 value. Only slot selection depends on it. */ @@ -514,9 +501,6 @@ unsigned long cache_hash_str(const char *str) unsigned long h = FNV_OFFSET; unsigned char *s = (unsigned char *)str; - if (!s) - return h; - while (*s) { h *= FNV_PRIME; h ^= *s++; @@ -524,6 +508,16 @@ unsigned long cache_hash_str(const char *str) return h; } +/* + * Unlike git's strbuf_complete, an empty path gains the slash too, so an empty + * cache-root keeps its slots under / rather than in the working directory. + */ +static void end_with_slash(struct strbuf *path) +{ + if (!path->len || path->buf[path->len - 1] != '/') + strbuf_addch(path, '/'); +} + int cache_process(int size, const char *path, const char *key, int ttl, size_t max_bytes, cache_fill_fn fn) { @@ -539,21 +533,14 @@ int cache_process(int size, const char *path, const char *key, int ttl, size_t m return 0; } - if (!path) { - log_error("[cgit] Cache path not specified, caching is disabled\n"); - fn(); - return 0; - } - if (!key) - key = ""; if (!key_fits_slot(key)) { - log_error("[cgit] Cache key too long for a slot, caching is disabled for this request\n"); + fprintf(stderr, "[cgit] Cache key too long for a slot, caching is disabled for this request\n"); fn(); return 0; } hash = cache_hash_str(key) % size; strbuf_addstr(&slot_path, path); - strbuf_ensure_end(&slot_path, '/'); + end_with_slash(&slot_path); for (i = 0; i < SLOT_NAME_LEN; i++) { strbuf_addf(&slot_path, "%x", (unsigned char)(hash & 0xf)); hash >>= 4; @@ -577,7 +564,7 @@ int cache_process(int size, const char *path, const char *key, int ttl, size_t m return result; } -int cache_ls(const char *path) +void cache_ls(const char *path) { DIR *dir; struct dirent *ent; @@ -590,42 +577,41 @@ int cache_ls(const char *path) char *nul; int keylen; - if (!path) { - log_error("[cgit] Cache path not specified\n"); - return -1; - } dir = opendir(path); if (!dir) { err = errno; - log_error("[cgit] Unable to open %s: %s (%d)\n", path, strerror(err), err); - return err; + fprintf(stderr, "[cgit] Unable to open %s: %s (%d)\n", path, strerror(err), err); + return; } strbuf_addstr(&slot_path, path); - strbuf_ensure_end(&slot_path, '/'); + end_with_slash(&slot_path); prefixlen = slot_path.len; while ((ent = readdir(dir))) { + char mtime[64] = "-"; + struct tm tm; + if (strlen(ent->d_name) != SLOT_NAME_LEN) continue; strbuf_setlen(&slot_path, prefixlen); strbuf_addstr(&slot_path, ent->d_name); slot.path = slot_path.buf; if ((err = open_slot(&slot)) != 0) { - log_error("[cgit] Unable to open %s: %s (%d)\n", slot_path.buf, strerror(err), err); - close_slot(&slot); + fprintf(stderr, "[cgit] Unable to open %s: %s (%d)\n", slot_path.buf, strerror(err), err); + close_fd(&slot.cache_fd); continue; } + if (slot.st.st_mtime && gmtime_r(&slot.st.st_mtime, &tm)) + strftime(mtime, sizeof(mtime), "%Y-%m-%d %H:%M:%S", &tm); // A truncated or corrupt slot may hold no NUL, so the print is // bounded by what was read and cannot run off the end. nul = memchr(slot.buf, 0, slot.buflen); keylen = nul ? (int)(nul - slot.buf) : slot.buflen; htmlf( - "%s %s %10"PRIuMAX" %.*s\n", slot_path.buf, - format_time("%Y-%m-%d %H:%M:%S", slot.st.st_mtime), + "%s %s %10"PRIuMAX" %.*s\n", slot_path.buf, mtime, (uintmax_t)slot.st.st_size, keylen, slot.buf ); - close_slot(&slot); + close_fd(&slot.cache_fd); } closedir(dir); strbuf_release(&slot_path); - return 0; } diff --git a/source/cache.h b/source/cache.h index d3a7ec4..cfe1fbf 100644 --- a/source/cache.h +++ b/source/cache.h @@ -27,7 +27,7 @@ extern int cache_process(int size, const char *path, const char *key, int ttl, * Write one line per cache slot to stdout, giving its path, modification time, * size and key. */ -extern int cache_ls(const char *path); +extern void cache_ls(const char *path); /* * Give up on the slot being filled and put stdout back on the client, after @@ -40,4 +40,14 @@ extern int cache_abandon_fill(void); extern unsigned long cache_hash_str(const char *str); +/* + * Open the lock file at path, creating it if need be, then lock it without + * waiting and empty it, leaving the descriptor in *fd. The lock holds until + * the last descriptor on the file closes, and the caller renames or removes + * the file before that. Returns 0, EAGAIN or EWOULDBLOCK when another process + * holds the lock or has renamed the file away, or the errno of the step that + * failed, which has been logged. *fd is -1 on failure. + */ +extern int cache_lock_file(const char *path, int *fd); + #endif // CGIT_CACHE_H 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) diff --git a/source/cgit.h b/source/cgit.h index 5e17a3e..da202b7 100644 --- a/source/cgit.h +++ b/source/cgit.h @@ -135,7 +135,6 @@ struct commitinfo { int committer_tz; char *subject; char *msg; - char *msg_encoding; }; struct taginfo { @@ -146,21 +145,6 @@ struct taginfo { char *msg; }; -struct refinfo { - const char *refname; - struct object *object; - union { - struct taginfo *tag; - struct commitinfo *commit; - }; -}; - -struct reflist { - struct refinfo **refs; - int alloc; - int count; -}; - struct cgit_query { int has_oid; int has_difftype; @@ -207,7 +191,6 @@ struct cgit_config { const char *root_title; const char *root_desc; char *root_readme; - const char *script_name; const char *section; const char *repository_sort; // Always ends with a slash. @@ -303,7 +286,6 @@ struct cgit_environment { const char *path_info; const char *query_string; const char *request_method; - const char *script_name; const char *server_name; const char *server_port; const char *http_cookie; diff --git a/source/cgit.mk b/source/cgit.mk index 0786086..66b94d3 100644 --- a/source/cgit.mk +++ b/source/cgit.mk @@ -4,9 +4,7 @@ # through CGIT_ROOT. The top level Makefile invokes it roughly as # # make -C vendor/git -f ../../source/cgit.mk ../../build/cgit -# -# so sources are read from ../../source and all output is written to -# ../../build. + include Makefile # BUILDDIR is named relative to the project root because the version recipe @@ -64,8 +62,11 @@ ifdef NO_LUA LUA_MESSAGE := linking without specified Lua support CGIT_CFLAGS += -DNO_LUA else +# Each system names Lua's package its own way: lua5.4 on Debian and Alpine, +# lua-5.4 on FreeBSD and NetBSD, lua54 on OpenBSD. ifeq ($(LUA_PKGCONFIG),) - LUA_PKGCONFIG := $(shell for pc in luajit lua lua5.5 lua5.4 lua5.3 lua5.2 lua5.1; do \ + LUA_PKGCONFIG := $(shell for pc in luajit lua lua5.5 lua5.4 lua5.3 lua5.2 lua5.1 \ + lua-5.5 lua-5.4 lua-5.3 lua-5.2 lua-5.1 lua55 lua54 lua53 lua52 lua51; do \ $(PKG_CONFIG) --exists $$pc 2>/dev/null && echo $$pc && break; \ done) LUA_MODE := autodetected @@ -158,9 +159,9 @@ $(CGIT_BUILD)/.depend: $(CGIT_BUILD)/CGIT-CFLAGS: FORCE @mkdir -p $(CGIT_BUILD)/ @FLAGS='$(subst ','\'',$(CGIT_CFLAGS))'; \ - OLD=`cat $(CGIT_BUILD)/CGIT-CFLAGS 2>/dev/null`; \ - if test x"$$FLAGS" != x"$$OLD"; then \ - echo 1>&2 " * new CGit build flags"; \ + OLD=$$(cat $(CGIT_BUILD)/CGIT-CFLAGS 2>/dev/null); \ + if test "$$FLAGS" != "$$OLD"; then \ + echo 1>&2 " * new cgit build flags"; \ echo "$$FLAGS" >$(CGIT_BUILD)/CGIT-CFLAGS; \ fi diff --git a/source/cmd.c b/source/cmd.c index 22a6c19..3fd393b 100644 --- a/source/cmd.c +++ b/source/cmd.c @@ -1,14 +1,13 @@ /* - * One thunk per page, each pulling the parts of the request its renderer - * needs out of the global ctx, plus the table that maps a page name onto its - * thunk. Keeping the thunks here lets every renderer take ordinary arguments - * instead of reaching into the request state itself. + * A renderer that reads the request itself goes into the page table as it is. + * The others sit behind a thunk here that pulls their arguments out of the + * global ctx or does some work first, such as checking that the page is + * enabled. */ #include "cache.h" #include "cgit.h" #include "cmd.h" -#include "html.h" #include "ui-atom.h" #include "ui-blame.h" #include "ui-blob.h" @@ -27,11 +26,6 @@ #include "ui-tag.h" #include "ui-tree.h" -static void head_fn(void) -{ - cgit_clone_head(); -} - static void about_fn(void) { char *query, *summary; @@ -75,7 +69,7 @@ static void blame_fn(void) static void blob_fn(void) { - cgit_print_blob(ctx.qry.oid, ctx.qry.path, ctx.qry.head, 0); + cgit_print_blob(ctx.qry.oid, ctx.qry.path, ctx.qry.head); } static void commit_fn(void) @@ -89,11 +83,6 @@ static void diff_fn(void) cgit_print_diff(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path, 1, 0); } -static void info_fn(void) -{ - cgit_clone_info(); -} - static void log_fn(void) { cgit_prefix_title("log"); @@ -122,21 +111,11 @@ static void ls_cache_fn(void) cache_ls(ctx.cfg.cache_root); } -static void objects_fn(void) -{ - cgit_clone_objects(); -} - static void patch_fn(void) { cgit_print_patch(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path); } -static void plain_fn(void) -{ - cgit_print_plain(); -} - static void rawdiff_fn(void) { cgit_print_diff(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path, 1, 1); @@ -148,11 +127,6 @@ static void refs_fn(void) cgit_print_refs(); } -static void repolist_fn(void) -{ - cgit_print_repolist(); -} - static void snapshot_fn(void) { cgit_print_snapshot(ctx.qry.head, ctx.qry.oid, ctx.qry.path, ctx.qry.nohead); @@ -168,11 +142,6 @@ static void stats_fn(void) } } -static void summary_fn(void) -{ - cgit_print_summary(); -} - static void tag_fn(void) { cgit_print_tag(ctx.qry.oid); @@ -186,27 +155,27 @@ static void tree_fn(void) // The name is what a request spells as p=, so these strings are part of the // URL space and cannot be renamed. static const struct cgit_cmd commands[] = { - { .name = "HEAD", .fn = head_fn, .want_repo = 1, .is_clone = 1 }, + { .name = "HEAD", .fn = cgit_clone_head, .want_repo = 1, .is_clone = 1 }, { .name = "about", .fn = about_fn }, { .name = "atom", .fn = atom_fn, .want_repo = 1 }, - { .name = "blame", .fn = blame_fn, .want_repo = 1, .want_vpath = 1 }, - { .name = "blob", .fn = blob_fn, .want_repo = 1 }, - { .name = "commit", .fn = commit_fn, .want_repo = 1, .want_vpath = 1 }, - { .name = "diff", .fn = diff_fn, .want_repo = 1, .want_vpath = 1 }, - { .name = "info", .fn = info_fn, .want_repo = 1, .is_clone = 1 }, + { .name = "blame", .fn = blame_fn, .want_repo = 1, .want_vpath = 1, .is_static = 1 }, + { .name = "blob", .fn = blob_fn, .want_repo = 1, .is_static = 1 }, + { .name = "commit", .fn = commit_fn, .want_repo = 1, .want_vpath = 1, .is_static = 1 }, + { .name = "diff", .fn = diff_fn, .want_repo = 1, .want_vpath = 1, .is_static = 1 }, + { .name = "info", .fn = cgit_clone_info, .want_repo = 1, .is_clone = 1 }, { .name = "log", .fn = log_fn, .want_repo = 1, .want_vpath = 1 }, { .name = "ls_cache", .fn = ls_cache_fn }, - { .name = "objects", .fn = objects_fn, .want_repo = 1, .is_clone = 1 }, - { .name = "patch", .fn = patch_fn, .want_repo = 1, .want_vpath = 1 }, - { .name = "plain", .fn = plain_fn, .want_repo = 1 }, - { .name = "rawdiff", .fn = rawdiff_fn, .want_repo = 1, .want_vpath = 1 }, + { .name = "objects", .fn = cgit_clone_objects, .want_repo = 1, .is_clone = 1 }, + { .name = "patch", .fn = patch_fn, .want_repo = 1, .want_vpath = 1, .is_static = 1 }, + { .name = "plain", .fn = cgit_print_plain, .want_repo = 1, .is_static = 1 }, + { .name = "rawdiff", .fn = rawdiff_fn, .want_repo = 1, .want_vpath = 1, .is_static = 1 }, { .name = "refs", .fn = refs_fn, .want_repo = 1 }, - { .name = "repolist", .fn = repolist_fn }, - { .name = "snapshot", .fn = snapshot_fn, .want_repo = 1 }, + { .name = "repolist", .fn = cgit_print_repolist }, + { .name = "snapshot", .fn = snapshot_fn, .want_repo = 1, .is_static = 1 }, { .name = "stats", .fn = stats_fn, .want_repo = 1, .want_vpath = 1 }, - { .name = "summary", .fn = summary_fn, .want_repo = 1 }, - { .name = "tag", .fn = tag_fn, .want_repo = 1 }, - { .name = "tree", .fn = tree_fn, .want_repo = 1, .want_vpath = 1 }, + { .name = "summary", .fn = cgit_print_summary, .want_repo = 1 }, + { .name = "tag", .fn = tag_fn, .want_repo = 1, .is_static = 1 }, + { .name = "tree", .fn = tree_fn, .want_repo = 1, .want_vpath = 1, .is_static = 1 }, }; const struct cgit_cmd *cgit_find_cmd(const char *name) diff --git a/source/cmd.h b/source/cmd.h index f47878b..a855574 100644 --- a/source/cmd.h +++ b/source/cmd.h @@ -14,6 +14,10 @@ struct cgit_cmd { const char *name; cgit_cmd_fn fn; unsigned int want_repo:1, want_vpath:1, is_clone:1; + // Set on the pages that 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. + unsigned int is_static:1; }; /* diff --git a/source/config.c b/source/config.c index b029613..49cd398 100644 --- a/source/config.c +++ b/source/config.c @@ -85,7 +85,7 @@ static int read_entry(FILE *f, struct strbuf *name, struct strbuf *value) } } -int config_file_parse(const char *filename, config_file_value_fn fn) +void config_file_parse(const char *filename, config_file_value_fn fn) { static int nesting; struct strbuf name = STRBUF_INIT; @@ -95,9 +95,9 @@ int config_file_parse(const char *filename, config_file_value_fn fn) // An include line calls back into here, so an include cycle would // recurse until the stack gave out. if (nesting > MAX_INCLUDE_NESTING) - return -1; + return; if (!(f = fopen(filename, "r"))) - return -1; + return; nesting++; while (read_entry(f, &name, &value)) fn(name.buf, value.buf); @@ -105,5 +105,4 @@ int config_file_parse(const char *filename, config_file_value_fn fn) fclose(f); strbuf_release(&name); strbuf_release(&value); - return 0; } diff --git a/source/config.h b/source/config.h index ab42e2f..3ddf974 100644 --- a/source/config.h +++ b/source/config.h @@ -7,10 +7,8 @@ #ifndef CGIT_CONFIG_H #define CGIT_CONFIG_H -#include "cgit.h" - typedef void (*config_file_value_fn)(const char *name, const char *value); -extern int config_file_parse(const char *filename, config_file_value_fn fn); +extern void config_file_parse(const char *filename, config_file_value_fn fn); #endif // CGIT_CONFIG_H diff --git a/source/filter.c b/source/filter.c index f2dfe12..6cc5e80 100644 --- a/source/filter.c +++ b/source/filter.c @@ -25,6 +25,31 @@ // The exec filter holding stdout, for cgit_abort_filters to take it back. static struct cgit_exec_filter *running_exec; +/* + * Each of these returns its argument untouched when the syscall it wraps + * succeeded, and dies with the message and errno otherwise. + */ +static int die_unless_zero(int result, const char *msg) +{ + if (result != 0) + die_errno("%s", msg); + return result; +} + +static int die_unless_positive(int result, const char *msg) +{ + if (result <= 0) + die_errno("%s", msg); + return result; +} + +static int die_unless_non_negative(int result, const char *msg) +{ + if (result < 0) + die_errno("%s", msg); + return result; +} + static int open_exec_filter(struct cgit_filter *base, va_list ap) { struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; @@ -34,12 +59,12 @@ static int open_exec_filter(struct cgit_filter *base, va_list ap) for (i = 0; i < filter->base.argument_count; i++) filter->argv[i + 1] = va_arg(ap, char *); - filter->old_stdout = cgit_die_unless_positive(dup(STDOUT_FILENO), "Unable to duplicate stdout"); + filter->old_stdout = die_unless_positive(dup(STDOUT_FILENO), "Unable to duplicate stdout"); // The child keeps the page's stdout, but not the descriptor the parent // needs it back from. fcntl(filter->old_stdout, F_SETFD, FD_CLOEXEC); - cgit_die_unless_zero(pipe(pipefd), "Unable to create pipe to subprocess"); - filter->pid = cgit_die_unless_non_negative(fork(), "Unable to create subprocess"); + die_unless_zero(pipe(pipefd), "Unable to create pipe to subprocess"); + filter->pid = die_unless_non_negative(fork(), "Unable to create subprocess"); if (filter->pid == 0) { close(pipefd[1]); if (dup2(pipefd[0], STDIN_FILENO) < 0) @@ -55,7 +80,7 @@ static int open_exec_filter(struct cgit_filter *base, va_list ap) _exit(EXEC_FAILED); } close(pipefd[0]); - cgit_die_unless_non_negative(dup2(pipefd[1], STDOUT_FILENO), "Unable to use pipe as stdout"); + die_unless_non_negative(dup2(pipefd[1], STDOUT_FILENO), "Unable to use pipe as stdout"); close(pipefd[1]); running_exec = filter; return 0; @@ -64,22 +89,16 @@ static int open_exec_filter(struct cgit_filter *base, va_list ap) static int close_exec_filter(struct cgit_filter *base) { struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base; - int i, exit_status = 0; + int exit_status = 0; running_exec = NULL; - cgit_die_unless_non_negative(dup2(filter->old_stdout, STDOUT_FILENO), "Unable to restore stdout"); + die_unless_non_negative(dup2(filter->old_stdout, STDOUT_FILENO), "Unable to restore stdout"); close(filter->old_stdout); - if (filter->pid < 0) - goto done; waitpid(filter->pid, &exit_status, 0); if (!WIFEXITED(exit_status)) die("Subprocess %s exited abnormally", filter->cmd); if (WEXITSTATUS(exit_status) == EXEC_FAILED) die("Unable to run filter %s", filter->cmd); - -done: - for (i = 0; i < filter->base.argument_count; i++) - filter->argv[i + 1] = NULL; return WEXITSTATUS(exit_status); } @@ -103,16 +122,13 @@ static void cleanup_exec_filter(struct cgit_filter *base) static struct cgit_filter *new_exec_filter(const char *cmd, int argument_count) { struct cgit_exec_filter *filter; - int argv_size; filter = xmalloc(sizeof(*filter)); cgit_exec_filter_init(filter, cgit_strdup_first_line(cmd), NULL); filter->base.argument_count = argument_count; // argv is the command, then a slot per argument, then the NULL that // execvp needs to find the end. - argv_size = (2 + argument_count) * sizeof(char *); - filter->argv = xmalloc(argv_size); - memset(filter->argv, 0, argv_size); + filter->argv = xcalloc(2 + argument_count, sizeof(char *)); filter->argv[0] = filter->cmd; return &filter->base; } @@ -126,15 +142,8 @@ void cgit_exec_filter_init(struct cgit_exec_filter *filter, const char *cmd, con filter->base.cleanup = cleanup_exec_filter; filter->cmd = cmd; filter->argv = argv; - filter->base.argument_count = 0; } -#ifdef NO_LUA -void cgit_init_filters(void) -{ -} -#endif - #ifndef NO_LUA struct lua_filter { struct cgit_filter base; @@ -279,19 +288,9 @@ static int script_html_url_arg(lua_State *lua_state) return emit_unfiltered(lua_state, html_url_arg); } -/* - * html_include returns whether it found the file, and calling it through a - * pointer that claims it returns nothing would be undefined, so the result is - * dropped in a wrapper of the right shape. - */ -static void include_and_discard_result(const char *filename) -{ - html_include(filename); -} - static int script_html_include(lua_State *lua_state) { - return emit_unfiltered(lua_state, include_and_discard_result); + return emit_unfiltered(lua_state, html_include); } static const struct { @@ -383,8 +382,7 @@ static struct cgit_filter *new_lua_filter(const char *cmd, int argument_count) { struct lua_filter *filter; - filter = xmalloc(sizeof(*filter)); - memset(filter, 0, sizeof(*filter)); + filter = xcalloc(1, sizeof(*filter)); filter->base.open = open_lua_filter; filter->base.close = close_lua_filter; filter->base.fprintfp = fprintf_lua_filter; @@ -394,6 +392,10 @@ static struct cgit_filter *new_lua_filter(const char *cmd, int argument_count) return &filter->base; } +#else +void cgit_init_filters(void) +{ +} #endif int cgit_open_filter(struct cgit_filter *filter, ...) @@ -477,35 +479,14 @@ int cgit_filter_holds_stdout(void) return running_exec != NULL; } -static const struct { - const char *prefix; - struct cgit_filter *(*create)(const char *cmd, int argument_count); -} filter_specs[] = { - { "exec", new_exec_filter }, -#ifndef NO_LUA - { "lua", new_lua_filter }, -#endif -}; - struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype) { - const char *colon; - size_t i; - size_t len; + const char *colon, *arg; int argument_count; if (!cmd || !cmd[0]) return NULL; - colon = strchr(cmd, ':'); - // Measured only once there is a colon to measure against, because - // subtracting from a null pointer is not something C defines. - len = colon ? (size_t)(colon - cmd) : 0; - // A single letter before the colon is a Windows drive letter, not a - // filter prefix. - if (len == 1) - colon = NULL; - switch (filtertype) { case AUTH: argument_count = 12; @@ -526,17 +507,20 @@ struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype) break; } - if (!colon) - return new_exec_filter(cmd, argument_count); - - for (i = 0; i < ARRAY_SIZE(filter_specs); i++) { - if (len == strlen(filter_specs[i].prefix) && !strncmp(filter_specs[i].prefix, cmd, len)) - return filter_specs[i].create(colon + 1, argument_count); - } - + if (skip_prefix(cmd, "exec:", &arg)) + return new_exec_filter(arg, argument_count); + if (skip_prefix(cmd, "lua:", &arg)) { #ifdef NO_LUA - if (len == 3 && !strncmp("lua", cmd, len)) die("Built without Lua, cannot run filter %s", cmd); +#else + return new_lua_filter(arg, argument_count); #endif - die("Invalid filter type: %.*s", (int)len, cmd); + } + + // A single letter before the colon is a Windows drive letter, not a + // filter prefix. + colon = strchr(cmd, ':'); + if (!colon || colon - cmd == 1) + return new_exec_filter(cmd, argument_count); + die("Invalid filter type: %.*s", (int)(colon - cmd), cmd); } diff --git a/source/html.c b/source/html.c index d13ac5b..bc7a722 100644 --- a/source/html.c +++ b/source/html.c @@ -16,7 +16,11 @@ #define HTML_WRITE_BUFSIZE (64 * 1024) // The percent encoding for each byte, with NULL marking the bytes a URL may -// carry as themselves. Those are the letters, the digits, and !$()*,-./:;@[]_~ +// carry as themselves. An ampersand or plus is legal in a path but encoded all +// the same, since a bare plus in a query argument decodes back as a space, and +// an ampersand in an attribute value can start a character reference and +// quietly turn "a©.txt" into a different filename. The bytes left as +// themselves are the letters, the digits, and !$()*,-./:;@[]_~ static const char *url_escape_table[256] = { "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", "%08", "%09", "%0A", "%0B", "%0C", "%0D", "%0E", "%0F", @@ -122,18 +126,6 @@ char *cgit_fmt(const char *format, ...) return buf[slot]; } -char *cgit_fmtalloc(const char *format, ...) -{ - struct strbuf sb = STRBUF_INIT; - va_list args; - - va_start(args, format); - strbuf_vaddf(&sb, format, args); - va_end(args); - - return strbuf_detach(&sb, NULL); -} - void html_flush(void) { size_t len = out_len; @@ -218,6 +210,24 @@ void html_vtxtf(const char *format, va_list ap) strbuf_release(&sb); } +/* + * The entity for a character that page text would otherwise read as markup, or + * NULL for any other character. + */ +static const char *txt_entity(int c) +{ + switch (c) { + case '<': + return "<"; + case '>': + return ">"; + case '&': + return "&"; + default: + return NULL; + } +} + void html_txt(const char *txt) { if (txt) @@ -234,15 +244,11 @@ ssize_t html_ntxt(const char *txt, size_t len) left = (ssize_t)len; while (p && *p && left--) { - int c = *p; - if (c == '<' || c == '>' || c == '&') { + const char *entity = txt_entity(*p); + + if (entity) { html_raw(txt, p - txt); - if (c == '>') - html(">"); - else if (c == '<') - html("<"); - else if (c == '&') - html("&"); + html(entity); txt = p + 1; } p++; @@ -308,16 +314,10 @@ void html_blob_txt(const char *txt, size_t len) const char *p; for (p = txt; p < end; p++) { - const char *entity = NULL, *esc = NULL; + const char *entity = txt_entity(*p), *esc = NULL; size_t width = 1; - if (*p == '<') - entity = "<"; - else if (*p == '>') - entity = ">"; - else if (*p == '&') - entity = "&"; - else + if (!entity) esc = html_line_break_escape(p, end, &width); if (!entity && !esc) continue; @@ -355,19 +355,17 @@ void html_attr(const char *txt) const char *p = txt; while (p && *p) { - int c = *p; - if (c == '<' || c == '>' || c == '\'' || c == '\"' || c == '&') { + const char *entity; + + if (*p == '\'') + entity = "'"; + else if (*p == '"') + entity = """; + else + entity = txt_entity(*p); + if (entity) { html_raw(txt, p - txt); - if (c == '>') - html(">"); - else if (c == '<') - html("<"); - else if (c == '\'') - html("'"); - else if (c == '"') - html("""); - else if (c == '&') - html("&"); + html(entity); txt = p + 1; } p++; @@ -382,11 +380,6 @@ void html_url_path(const char *txt) while (p && *p) { unsigned char c = *p; - // A raw ampersand or plus is legal in a URL path, but this - // table is shared with html_url_arg, where a bare plus decodes - // back as a space, and the paths written here land in attribute - // values, where an ampersand can start a character reference - // and quietly turn "a©.txt" into a different filename. const char *esc = url_escape_table[c]; if (esc) { html_raw(txt, p - txt); @@ -511,36 +504,7 @@ void html_intoption(int value, const char *text, int selected_value) html("</option>\n"); } -void html_link_open(const char *url, const char *title, const char *class) -{ - html("<a href='"); - html_attr(url); - if (title) { - html("' title='"); - html_attr(title); - } - if (class) { - html("' class='"); - html_attr(class); - } - html("'>"); -} - -void html_link_close(void) -{ - html("</a>"); -} - -/* - * Render one permission triplet, so a caller prints a whole mode by passing it - * shifted right by six, then by three, then unshifted. - */ -void html_fileperm(unsigned short mode) -{ - htmlf("%c%c%c", (mode & 4 ? 'r' : '-'), (mode & 2 ? 'w' : '-'), (mode & 1 ? 'x' : '-')); -} - -int html_include(const char *filename) +void html_include(const char *filename) { FILE *f; char buf[4096]; @@ -548,27 +512,9 @@ int html_include(const char *filename) if (!(f = fopen(filename, "r"))) { fprintf(stderr, "[cgit] Unable to include %s: %s (%d)\n", filename, strerror(errno), errno); - return -1; + return; } while ((len = fread(buf, 1, sizeof(buf), f)) > 0) html_raw(buf, len); fclose(f); - return 0; -} - -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); - } } diff --git a/source/html.h b/source/html.h index 9cad3a6..d3cd8fe 100644 --- a/source/html.h +++ b/source/html.h @@ -92,16 +92,8 @@ extern void html_hidden(const char *name, const char *value); __attribute__((format (printf,1,2))) extern char *cgit_fmt(const char *format, ...); -__attribute__((format (printf,1,2))) -extern char *cgit_fmtalloc(const char *format, ...); - extern void html_option(const char *value, const char *text, const char *selected_value); extern void html_intoption(int value, const char *text, int 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_fileperm(unsigned short mode); -extern int html_include(const char *filename); - -extern void http_parse_querystring(const char *txt, void (*fn)(const char *name, const char *value)); +extern void html_include(const char *filename); #endif // CGIT_HTML_H diff --git a/source/parsing.c b/source/parsing.c index 71ecdb7..f684241 100644 --- a/source/parsing.c +++ b/source/parsing.c @@ -4,7 +4,8 @@ * the request path settles which repository and which page the request works * on. Commit and tag objects arrive as header lines, a blank line, and then * the message, so the readers here walk the headers, copy out the fields the - * pages need, and re-encode the message into the page encoding. + * pages need, and re-encode the message into the page encoding. The records + * they return are freed here too. */ #define USE_THE_REPOSITORY_VARIABLE @@ -15,22 +16,6 @@ #include <mailmap.h> -static char *substr(const char *start, const char *end) -{ - size_t len; - char *buf; - - if (end < start) - return xstrdup(""); - // start points into the object buffer, so strlcpy would measure the - // whole rest of the object to copy a name off the front of it. - len = end - start; - buf = xmalloc(len + 1); - memcpy(buf, start, len); - buf[len] = '\0'; - return buf; -} - // The repository's mailmap, read once per request and applied to every ident a // page shows. Only the blob at HEAD is read, the way git reads a bare // repository, so a repository cannot point the reader at a file on the server. @@ -112,22 +97,21 @@ static int end_of_header(const char *p) * not UTF-8 has its stray bytes replaced, and a page declared UTF-8 never * carries any. */ -static const char *reencode(char **text, const char *from, const char *to) +static void reencode(char **text, const char *encoding) { char *converted; - if (!*text || !from || !to) - return *text; + if (!*text) + return; - if (strcasecmp(from, to)) { - converted = reencode_string(*text, to, from); + if (strcasecmp(encoding, PAGE_ENCODING)) { + converted = reencode_string(*text, PAGE_ENCODING, encoding); if (converted) { free(*text); *text = converted; } } cgit_utf8_sanitize(*text); - return *text; } /* @@ -182,6 +166,7 @@ struct commitinfo *cgit_parse_commit(struct commit *commit) struct commitinfo *info; const char *p = repo_get_commit_buffer(the_repository, commit, NULL); const char *eol; + char *encoding = NULL; info = xcalloc(1, sizeof(struct commitinfo)); info->commit = commit; @@ -210,15 +195,15 @@ struct commitinfo *cgit_parse_commit(struct commit *commit) if (p && skip_prefix(p, "encoding ", &p)) { eol = strchr(p, '\n'); if (eol) { - info->msg_encoding = substr(p, eol); + encoding = xmemdupz(p, eol - p); p = eol + 1; } } // Git only writes the header when the message is in something other // than UTF-8, so its absence means UTF-8. - if (!info->msg_encoding) - info->msg_encoding = xstrdup("UTF-8"); + if (!encoding) + encoding = xstrdup("UTF-8"); while (!end_of_header(p)) p = next_header_line(p); @@ -227,7 +212,7 @@ struct commitinfo *cgit_parse_commit(struct commit *commit) if (p) { eol = strchrnul(p, '\n'); - info->subject = substr(p, eol); + info->subject = xmemdupz(p, eol - p); while (*eol == '\n') eol++; info->msg = xstrdup(eol); @@ -238,16 +223,28 @@ struct commitinfo *cgit_parse_commit(struct commit *commit) info->msg = xstrdup(""); } - reencode(&info->author, info->msg_encoding, PAGE_ENCODING); - reencode(&info->author_email, info->msg_encoding, PAGE_ENCODING); - reencode(&info->committer, info->msg_encoding, PAGE_ENCODING); - reencode(&info->committer_email, info->msg_encoding, PAGE_ENCODING); - reencode(&info->subject, info->msg_encoding, PAGE_ENCODING); - reencode(&info->msg, info->msg_encoding, PAGE_ENCODING); + reencode(&info->author, encoding); + reencode(&info->author_email, encoding); + reencode(&info->committer, encoding); + reencode(&info->committer_email, encoding); + reencode(&info->subject, encoding); + reencode(&info->msg, encoding); + free(encoding); return info; } +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); +} + struct taginfo *cgit_parse_tag(struct tag *tag) { void *data; @@ -283,3 +280,16 @@ cleanup: free(data); return info; } + +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); +} diff --git a/source/parsing.h b/source/parsing.h index 755d646..c0bdebc 100644 --- a/source/parsing.h +++ b/source/parsing.h @@ -2,7 +2,8 @@ * Readers that turn raw git object bodies and the incoming request URL into * the records cgit renders from. Commit and tag objects arrive as flat text, * so the work here is splitting off the headers and re-encoding the remaining - * free text into the page encoding. + * free text into the page encoding. Each record is freed beside the reader that + * builds it. */ #ifndef CGIT_PARSING_H @@ -18,6 +19,8 @@ extern void cgit_parse_url(const char *url); extern struct string_list *cgit_mailmap(void); extern struct commitinfo *cgit_parse_commit(struct commit *commit); +extern void cgit_free_commitinfo(struct commitinfo *info); extern struct taginfo *cgit_parse_tag(struct tag *tag); +extern void cgit_free_taginfo(struct taginfo *info); #endif // CGIT_PARSING_H diff --git a/source/scan-tree.c b/source/scan-tree.c index 10f661d..ec991e0 100644 --- a/source/scan-tree.c +++ b/source/scan-tree.c @@ -1,11 +1,7 @@ /* - * Discovery of repositories on disk, so that an administrator can point cgit - * at a directory instead of naming every repository in cgitrc. The tree below - * that directory is walked for bare repositories and working trees, or when a - * project list is configured only the paths it names are visited. Every - * repository found is registered under its path relative to the root of the - * scan, with owner, description and section taken from the files git keeps - * beside it, and a cgitrc in the repository has the last word over all of them. + * Every repository found takes its owner, description and section from the + * files git keeps beside it, and a cgitrc in the repository has the last word + * over all of them. */ #include "cgit.h" @@ -154,7 +150,6 @@ static void add_repo(const char *base, struct strbuf *path) size_t pathlen; struct strbuf relpath = STRBUF_INIT; char *comma; - size_t desc_size; if (stat(path->buf, &st)) { fprintf(stderr, "[cgit] Unable to access %s: %s (%d)\n", path->buf, strerror(errno), errno); @@ -185,23 +180,20 @@ static void add_repo(const char *base, struct strbuf *path) // Drop the trailing slash added above, or the "/.git" suffix test // below never matches and a working tree is named "repo/.git". - if (relpath.len && relpath.buf[relpath.len - 1] == '/') - strbuf_setlen(&relpath, relpath.len - 1); - if (relpath.len >= 5 && !strcmp(relpath.buf + relpath.len - 5, "/.git")) - strbuf_setlen(&relpath, relpath.len - 5); - else if (!strcmp(relpath.buf, ".git")) + strbuf_strip_suffix(&relpath, "/"); + if (!strbuf_strip_suffix(&relpath, "/.git") && !strcmp(relpath.buf, ".git")) strbuf_setlen(&relpath, 0); // The scan root may itself be the repository, leaving nothing after // the base, so that one is named after its directory. if (!relpath.len) { - const char *end = path->buf + pathlen - 1, *start; + size_t len = pathlen - 1; + const char *start; - if (end - path->buf >= 5 && !strncmp(end - 5, "/.git", 5)) - end -= 5; - start = end; + strip_suffix_mem(path->buf, &len, "/.git"); + start = path->buf + len; while (start > path->buf && start[-1] != '/') start--; - strbuf_add(&relpath, start, end - start); + strbuf_add(&relpath, start, path->buf + len - start); } current_repo = cgit_add_repo(relpath.buf); @@ -226,23 +218,27 @@ static void add_repo(const char *base, struct strbuf *path) strip_suffix_mem(current_repo->url, &urllen, "/"); current_repo->url[urllen] = '\0'; } - while (!current_repo->owner) { + if (!current_repo->owner) { + // A uid with no passwd entry is no error, and getpwuid tells + // the two apart only by setting errno on a failure. + errno = 0; if (!(pwd = getpwuid(st.st_uid))) { - fprintf(stderr, "[cgit] Unable to read the owner of %s: %s (%d)\n", - path->buf, strerror(errno), errno); - break; + if (errno) + fprintf(stderr, "[cgit] Unable to read the owner of %s: %s (%d)\n", + path->buf, strerror(errno), errno); + } else { + // A gecos field puts the owner's name in front of a + // comma separated list of office and phone details. + if (pwd->pw_gecos && (comma = strchr(pwd->pw_gecos, ','))) + *comma = '\0'; + current_repo->owner = + cgit_strdup_first_line(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name); } - // A gecos field puts the owner's name in front of a comma - // separated list of office and phone details. - if (pwd->pw_gecos && (comma = strchr(pwd->pw_gecos, ','))) - *comma = '\0'; - current_repo->owner = cgit_strdup_first_line(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name); } - if (current_repo->desc == cgit_default_repo_desc || !current_repo->desc) { + if (current_repo->desc == cgit_default_repo_desc) { strbuf_addstr(path, "description"); - if (!stat(path->buf, &st)) - cgit_read_first_line(path->buf, ¤t_repo->desc, &desc_size); + cgit_read_first_line(path->buf, ¤t_repo->desc); strbuf_setlen(path, pathlen); if (current_repo->desc && !strcmp(current_repo->desc, default_git_desc)) { free(current_repo->desc); @@ -254,8 +250,7 @@ static void add_repo(const char *base, struct strbuf *path) set_section_from_path(&relpath, ctx.cfg.section_from_path); strbuf_addstr(path, "cgitrc"); - if (!stat(path->buf, &st)) - config_file_parse(path->buf, apply_cgitrc); + config_file_parse(path->buf, apply_cgitrc); strbuf_release(&relpath); } @@ -344,7 +339,7 @@ end: closedir(dir); } -void scan_projects(const char *path, const char *projectsfile) +static void scan_projects(const char *path, const char *projectsfile) { struct strbuf line = STRBUF_INIT; FILE *projects; @@ -374,5 +369,8 @@ void scan_projects(const char *path, const char *projectsfile) void scan_tree(const char *path) { - scan_path(path, path); + if (ctx.cfg.project_list) + scan_projects(path, ctx.cfg.project_list); + else + scan_path(path, path); } diff --git a/source/scan-tree.h b/source/scan-tree.h index 515037a..e29f8bf 100644 --- a/source/scan-tree.h +++ b/source/scan-tree.h @@ -10,7 +10,6 @@ #ifndef CGIT_SCAN_TREE_H #define CGIT_SCAN_TREE_H -extern void scan_projects(const char *path, const char *projectsfile); extern void scan_tree(const char *path); #endif // CGIT_SCAN_TREE_H diff --git a/source/shared.c b/source/shared.c index a20773b..1e1bb26 100644 --- a/source/shared.c +++ b/source/shared.c This diff is too large to be rendered inline. View it on its own page. diff --git a/source/shared.h b/source/shared.h index 2bf305d..302b752 100644 --- a/source/shared.h +++ b/source/shared.h @@ -1,9 +1,8 @@ /* - * The helpers that belong to no single page. Repository registration, the - * lifetimes of the reference and commit records the renderers build, thin - * wrappers over git's diff machinery, and a handful of string, date and - * mimetype utilities all live here because several renderers need them. The - * two process-wide globals, ctx and cgit_repolist, are defined here as well. + * The helpers that more than one file needs. Repository registration and thin + * wrappers over git's diff machinery make up most of it, beside a few string + * and request checks. The two process-wide globals, ctx and cgit_repolist, + * are defined here as well. */ #ifndef CGIT_SHARED_H @@ -17,14 +16,6 @@ typedef void (*filepair_fn)(struct diff_filepair *pair); typedef void (*linediff_fn)(char *line, int len); /* - * Each of these returns its argument untouched when the syscall it wraps - * succeeded, and dies with the message and errno otherwise. - */ -extern int cgit_die_unless_zero(int result, const char *msg); -extern int cgit_die_unless_positive(int result, const char *msg); -extern int cgit_die_unless_non_negative(int result, const char *msg); - -/* * Append a repository to cgit_repolist and return it, seeded from the current * global configuration. The returned pointer is invalidated by the next call, * because the list grows by reallocation. @@ -33,22 +24,6 @@ extern struct cgit_repo *cgit_add_repo(const char *url); extern struct cgit_repo *cgit_get_repoinfo(const char *url); /* - * Export the CGIT_REPO_* variables that filters and hooks read. - */ -extern void cgit_prepare_repo_env(struct cgit_repo *repo); - -extern void cgit_free_reflist_inner(struct reflist *list); - -/* - * Matches git's reference iteration callback, and appends each ref it is given - * to the struct reflist passed as cb_data. - */ -extern int cgit_refs_cb(const struct reference *ref, void *cb_data); - -extern void cgit_free_commitinfo(struct commitinfo *info); -extern void cgit_free_taginfo(struct taginfo *info); - -/* * A diff format callback that hands each filepair to the filepair_fn data * points at. ISO C has no conversion between a function pointer and void *, * so the function travels by address. @@ -66,13 +41,6 @@ extern int cgit_diff_files(const struct object_id *old_oid, const struct object_ linediff_fn fn); /* - * Whether a blob of size bytes is past max-blob-size. Callers ask before - * reading the object, so a huge blob stays out of memory rather than being - * noticed once it is already there. - */ -extern int cgit_blob_too_large(size_t size); - -/* * Limit a diff to the paths below prefix, which is taken literally. */ extern void cgit_diff_limit_path(struct diff_options *opt, const char *prefix); @@ -82,23 +50,28 @@ extern void cgit_diff_tree(const struct object_id *old_oid, const struct object_ extern void cgit_diff_commit(struct commit *commit, filepair_fn fn, const char *prefix); /* - * Accept only the date formats cgit documents, leaving mode unchanged for - * anything else. + * Whether a blob of size bytes is past max-blob-size. Callers ask before + * reading the object, so a huge blob stays out of memory rather than being + * noticed once it is already there. + */ +extern int cgit_blob_too_large(size_t size); + +/* + * Point ps at item alone, set to match path literally rather than as a glob, + * or at no item when path is NULL, which leaves every path matching. */ -extern void cgit_parse_date_format(const char *format, struct date_mode *mode); +extern void cgit_literal_pathspec(struct pathspec *ps, struct pathspec_item *item, const char *path); /* * Copy str without its trailing runs of c, returning NULL if nothing is left. */ extern char *cgit_trim_end(const char *str, char c); -extern char *cgit_ensure_end(const char *str, char c); -extern void strbuf_ensure_end(struct strbuf *sb, char c); /* * Read the first line of a file into a newly allocated, zero terminated * buffer. Returns 0 on success and an errno value otherwise. */ -extern int cgit_read_first_line(const char *path, char **buf, size_t *size); +extern int cgit_read_first_line(const char *path, char **buf); extern char *cgit_strdup_first_line(const char *text); /* @@ -119,8 +92,6 @@ extern void cgit_utf8_sanitize(char *text); */ extern char *cgit_expand_macros(const char *text); -extern char *cgit_get_mimetype_for_filename(const char *filename); - /* * Whether a readme setting names a file tracked in the repository, written * as ref:path or :path, rather than a file on the server's disk. @@ -140,16 +111,4 @@ extern int cgit_valid_rev(const char *rev); */ extern int cgit_notes_ref_usable(const char *notes_ref); -/* - * qsort for a list that may be empty. C leaves a null base undefined even - * with nothing to sort, and glibc declares it nonnull, while the lists here - * stay null until their first entry. - */ -static inline void cgit_qsort(void *base, size_t n, size_t size, - int (*cmp)(const void *, const void *)) -{ - if (n) - qsort(base, n, size, cmp); -} - #endif // CGIT_SHARED_H diff --git a/source/ui-atom.c b/source/ui-atom.c index 55b59a1..b9762b1 100644 --- a/source/ui-atom.c +++ b/source/ui-atom.c @@ -105,7 +105,6 @@ static void print_entry(struct commit *commit, const char *host) { struct commitinfo *info; char *hex, *pageurl; - char delim = '&'; info = cgit_parse_commit(commit); hex = oid_to_hex(&commit->object.oid); @@ -135,15 +134,8 @@ static void print_entry(struct commit *commit, const char *host) xml_txt(feed_date(info->author_date)); html("</published>\n"); html("<link rel='alternate' type='text/html' href='"); - html(cgit_httpscheme()); - html_attr(host); - pageurl = cgit_pageurl(ctx.repo->url, "commit", NULL); - html_attr(pageurl); - // Without a virtual root the page url is already a query string, so - // the commit id continues it instead of opening one. - if (ctx.cfg.virtual_root) - delim = '?'; - html_attrf("%cid=%s", delim, hex); + pageurl = cgit_pageurl(ctx.repo->url, "commit", cgit_fmt("id=%s", hex)); + html_attrf("%s%s%s", cgit_httpscheme(), host, pageurl); html("'/>\n"); free(pageurl); html("<id>"); @@ -168,13 +160,8 @@ void cgit_print_atom(char *tip, const char *path, int max_count) int argc = 2; bool need_updated = true; - // A leading colon makes git read the path as a pathspec with magic, - // which can end the request inside git, so the feed refuses it as the - // log does. - if (path && path[0] == ':') { - cgit_print_error_page(400, "Bad Request", "Invalid path"); + if (cgit_refuse_pathspec_magic(path)) return; - } if (ctx.qry.show_all) argv[1] = "--all"; @@ -187,18 +174,10 @@ void cgit_print_atom(char *tip, const char *path, int max_count) } repo_init_revisions(the_repository, &rev, NULL); - rev.abbrev = DEFAULT_ABBREV; - rev.commit_format = CMIT_FMT_DEFAULT; - rev.verbose_header = 1; - rev.show_root_diff = 0; rev.max_count = max_count; setup_revisions(argc, argv, &rev, NULL); - // A failed setup leaves the walk holding freed commits, so it must - // not be read from. - if (prepare_revision_walk(&rev)) { - cgit_print_error_page(500, "Internal Server Error", "Unable to read the history"); + if (cgit_prepare_walk(&rev)) return; - } // CGI guarantees a server name, so only a bare test run reaches the // fallback, which keeps the mandatory feed id and links present. @@ -230,10 +209,10 @@ void cgit_print_atom(char *tip, const char *path, int max_count) html("<id>"); xml_txt(idbuf.buf); html("</id>\n"); - strbuf_release(&idbuf); html("<link rel='self' href='"); - html_attrf("%s%s%s", cgit_httpscheme(), host, fullurl); + html_attr(idbuf.buf); html("'/>\n"); + strbuf_release(&idbuf); html("<link rel='alternate' type='text/html' href='"); html_attrf("%s%s%s", cgit_httpscheme(), host, repourl); html("'/>\n"); diff --git a/source/ui-blame.c b/source/ui-blame.c index 48cbf8e..eef8602 100644 --- a/source/ui-blame.c +++ b/source/ui-blame.c @@ -24,7 +24,7 @@ enum blame_target { }; struct walk_tree_context { - char *rev; + const char *rev; int match_baselen; enum blame_target found; }; @@ -34,15 +34,6 @@ struct walk_tree_context { // detail is cached here and looked up by object id. static struct string_list suspect_details = STRING_LIST_INIT_DUP; -static void free_suspect_details(void) -{ - struct string_list_item *item; - - for_each_string_list_item(item, &suspect_details) - free(item->util); - string_list_clear(&suspect_details, 0); -} - /* * The scoreboard keeps a pointer to revs, so the caller owns both and has to * keep them alive together. @@ -57,7 +48,8 @@ static void run_blame(struct blame_scoreboard *sb, struct rev_info *revs, strvec_push(&argv, rev); repo_init_revisions(the_repository, revs, NULL); revs->diffopt.flags.allow_textconv = 1; - setup_revisions(argv.nr, argv.v, revs, NULL); + setup_revisions_from_strvec(&argv, revs, NULL); + strvec_clear(&argv); init_scoreboard(sb); sb->revs = revs; sb->repo = the_repository; @@ -67,7 +59,6 @@ static void run_blame(struct blame_scoreboard *sb, struct rev_info *revs, prio_queue_put(&sb->commits, origin->commit); blame_origin_decref(origin); sb->ent = NULL; - sb->path = path; assign_blame(sb, 0); blame_sort_final(sb); blame_coalesce(sb); @@ -161,44 +152,20 @@ static void emit_entry_hash(struct blame_entry *ent) emit_chars('\n', ent->num_lines); } -static void emit_hashes(struct blame_scoreboard *sb) -{ - struct blame_entry *ent; - - html("<td class='hashes'>"); - for (ent = sb->ent; ent; ent = ent->next) { - html("<div class='alt'><pre>"); - emit_entry_hash(ent); - html("</pre></div>"); - } - html("</td>\n"); -} - static void emit_entry_linenumbers(struct blame_entry *ent) { - struct strbuf numbers = STRBUF_INIT; - int lineno = ent->lno; - - while (lineno < ent->lno + ent->num_lines) { - lineno++; - strbuf_addf(&numbers, "<a id='n%d' href='#n%d'>%d</a>\n", lineno, lineno, lineno); - if (numbers.len >= HTML_BATCH) { - html_raw(numbers.buf, numbers.len); - strbuf_reset(&numbers); - } - } - html_raw(numbers.buf, numbers.len); - strbuf_release(&numbers); + cgit_print_line_anchors((size_t)ent->lno + 1, (size_t)ent->lno + ent->num_lines); } -static void emit_linenumbers(struct blame_scoreboard *sb) +static void emit_column(struct blame_scoreboard *sb, const char *class, + void (*emit_entry)(struct blame_entry *ent)) { struct blame_entry *ent; - html("<td class='linenumbers'>"); + htmlf("<td class='%s'>", class); for (ent = sb->ent; ent; ent = ent->next) { html("<div class='alt'><pre>"); - emit_entry_linenumbers(ent); + emit_entry(ent); html("</pre></div>"); } html("</td>\n"); @@ -220,9 +187,10 @@ static size_t line_width(struct blame_scoreboard *sb, int line) pos += len; continue; } - width++; if (*pos++ == '\t') - width = (width + TAB_WIDTH - 1) & ~(TAB_WIDTH - 1); + width += TAB_WIDTH - width % TAB_WIDTH; + else + width++; } return width; } @@ -275,8 +243,9 @@ static void print_blame_page(const struct object_id *oid, const char *path, enum object_type type; char *buf; size_t size; - struct rev_info revs; + struct rev_info revs = REV_INFO_INIT; struct blame_scoreboard sb; + int binary; type = odb_read_object_info(the_repository->objects, oid, &size); if (type == OBJ_BAD) { @@ -295,7 +264,13 @@ static void print_blame_page(const struct object_id *oid, const char *path, return; } - run_blame(&sb, &revs, path, rev); + // A NUL past the window buffer_is_binary sniffs would end a Lua + // filter's output early while the hash and line number columns still + // cover the whole file, so a blob holding one anywhere is treated as + // binary too. + binary = buffer_is_binary(buf, size) || memchr(buf, 0, size); + if (!binary) + run_blame(&sb, &revs, path, rev); cgit_set_title_from_path(path); cgit_prefix_title("blame"); @@ -307,29 +282,16 @@ static void print_blame_page(const struct object_id *oid, const char *path, cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path); html(")</div>\n"); - // A NUL past the window buffer_is_binary sniffs would end a Lua - // filter's output early while the hash and line number columns still - // cover the whole file, so a blob holding one anywhere is treated as - // binary too. - if (buffer_is_binary(buf, size) || memchr(buf, 0, size)) { - struct blame_entry *ent = sb.ent, *next; - - // The scoreboard is normally consumed by the column passes, - // so this early exit has to release it itself. - for (; ent; ent = next) { - next = ent->next; - free(ent); - } - free((void *)sb.final_buf); + if (binary) { html("<div class='error'>Blob is binary</div>"); goto cleanup; } html("<table class='blame blob'>\n<tr>\n"); - emit_hashes(&sb); + emit_column(&sb, "hashes", emit_entry_hash); if (ctx.cfg.enable_tree_linenumbers) - emit_linenumbers(&sb); + emit_column(&sb, "linenumbers", emit_entry_linenumbers); html("<td class='lines'><div>"); emit_line_backgrounds(&sb); @@ -338,12 +300,9 @@ static void print_blame_page(const struct object_id *oid, const char *path, html("<pre><code>"); if (ctx.repo->source_filter) { - char *filter_arg = xstrdup(filename); - - cgit_open_filter(ctx.repo->source_filter, filter_arg); + cgit_open_filter(ctx.repo->source_filter, filename); html_raw(buf, size); cgit_close_filter(ctx.repo->source_filter); - free(filter_arg); } else { html_blob_txt(buf, size); } @@ -355,8 +314,9 @@ static void print_blame_page(const struct object_id *oid, const char *path, cleanup: cgit_print_layout_end(); - free_suspect_details(); + string_list_clear(&suspect_details, 1); free(buf); + release_revisions(&revs); } static int walk_tree(const struct object_id *oid, struct strbuf *base, @@ -399,18 +359,8 @@ void cgit_print_blame(void) const char *rev = ctx.qry.oid; struct object_id oid; struct commit *commit; - int path_len = ctx.qry.path ? strlen(ctx.qry.path) : 0; - // nowildcard_len matches len so git treats the path as literal rather - // than as a glob, which is what a request naming one file means. - struct pathspec_item path_items = { - .match = ctx.qry.path, - .len = path_len, - .nowildcard_len = path_len - }; - struct pathspec paths = { - .nr = 1, - .items = &path_items - }; + struct pathspec_item path_item; + struct pathspec paths = { 0 }; struct walk_tree_context walk = { .found = TARGET_MISSING }; @@ -418,24 +368,17 @@ void cgit_print_blame(void) if (!rev) rev = ctx.qry.head; - if (repo_get_oid(the_repository, rev, &oid)) { - cgit_print_error_page(404, "Not Found", "Bad object id: %s", rev); + commit = cgit_get_commit(rev, &oid); + if (!commit) return; - } - commit = lookup_commit_reference(the_repository, &oid); - if (!commit || repo_parse_commit(the_repository, commit)) { - cgit_print_error_page(404, "Not Found", "Not a commit: %s", rev); - return; - } - walk.rev = xstrdup(rev); - walk.match_baselen = path_items.match ? basedir_len(path_items.match) : -1; + walk.rev = rev; + walk.match_baselen = ctx.qry.path ? basedir_len(ctx.qry.path) : -1; + cgit_literal_pathspec(&paths, &path_item, ctx.qry.path); read_tree(the_repository, repo_get_commit_tree(the_repository, commit), &paths, walk_tree, &walk); if (walk.found == TARGET_MISSING) cgit_print_error_page(404, "Not Found", "Not found"); else if (walk.found == TARGET_FOLDER) cgit_print_error_page(404, "Not Found", "Blame is not available for a directory"); - - free(walk.rev); } diff --git a/source/ui-blob.c b/source/ui-blob.c index 4690bb5..4ed9b3b 100644 --- a/source/ui-blob.c +++ b/source/ui-blob.c @@ -34,10 +34,6 @@ static void peel_to_commit(struct object_id *oid) oidcpy(oid, &commit->object.oid); } -/* - * read_tree reads the return value as a direction rather than a status, so - * READ_TREE_RECURSIVE means step into this entry and zero means step over it. - */ static int walk_tree(const struct object_id *oid, struct strbuf *base, const char *pathname, unsigned mode, void *context) { @@ -59,24 +55,14 @@ static int walk_tree(const struct object_id *oid, struct strbuf *base, /* * oid comes in naming a commit and goes out naming the blob found at path, - * which every caller relies on. The path has to be writable because a pathspec - * item does not hold a const string. + * which every caller relies on. */ -static int find_path_oid(struct object_id *oid, char *path, int file_only) +static int find_path_oid(struct object_id *oid, const char *path, int file_only) { struct commit *commit = lookup_commit_reference(the_repository, oid); struct tree *tree; - // nowildcard_len matching len makes git treat the path as literal - // rather than as a glob. - struct pathspec_item item = { - .match = path, - .len = strlen(path), - .nowildcard_len = strlen(path) - }; - struct pathspec paths = { - .nr = 1, - .items = &item - }; + struct pathspec_item item; + struct pathspec paths = { 0 }; struct walk_tree_context walk = { .match_path = path, .matched_oid = oid, @@ -91,30 +77,25 @@ static int find_path_oid(struct object_id *oid, char *path, int file_only) tree = repo_get_commit_tree(the_repository, commit); if (!tree) return 0; + cgit_literal_pathspec(&paths, &item, path); read_tree(the_repository, tree, &paths, walk_tree, &walk); return walk.found_path; } -int cgit_ref_path_exists(const char *path, const char *ref, int file_only) +int cgit_ref_path_exists(const char *path, const char *ref) { struct object_id oid; size_t size; - char *path_copy = xstrdup(path); - int found = 0; if (repo_get_oid(the_repository, ref, &oid)) - goto done; + return 0; peel_to_commit(&oid); if (odb_read_object_info(the_repository->objects, &oid, &size) != OBJ_COMMIT) - goto done; - found = find_path_oid(&oid, path_copy, file_only); - -done: - free(path_copy); - return found; + return 0; + return find_path_oid(&oid, path, 1); } -int cgit_print_file(char *path, const char *head, int file_only, int html_escape) +int cgit_print_file(char *path, const char *head, int html_escape) { struct object_id oid; enum object_type type; @@ -126,7 +107,7 @@ int cgit_print_file(char *path, const char *head, int file_only, int html_escape peel_to_commit(&oid); type = odb_read_object_info(the_repository->objects, &oid, &size); if (type == OBJ_COMMIT) { - if (!find_path_oid(&oid, path, file_only)) + if (!find_path_oid(&oid, path, 1)) return -1; type = odb_read_object_info(the_repository->objects, &oid, &size); } @@ -138,9 +119,8 @@ int cgit_print_file(char *path, const char *head, int file_only, int html_escape if (!buf) return -1; - // html_txt wants a terminated string, and git leaves a spare byte - // past every object it reads, so this write stays in the allocation. - buf[size] = '\0'; + // html_txt wants a terminated string, which git leaves past every + // object it reads. if (html_escape) { // html_txt stops at a NUL, so a blob holding one is written // segment by segment with a question mark standing in for @@ -162,7 +142,7 @@ int cgit_print_file(char *path, const char *head, int file_only, int html_escape return 0; } -void cgit_print_blob(const char *hex, char *path, const char *head, int file_only) +void cgit_print_blob(const char *hex, char *path, const char *head) { struct object_id oid; enum object_type type; @@ -191,7 +171,7 @@ void cgit_print_blob(const char *hex, char *path, const char *head, int file_onl } if (type == OBJ_COMMIT && path) { - if (!find_path_oid(&oid, path, file_only)) { + if (!find_path_oid(&oid, path, 0)) { cgit_print_error_page(404, "Not Found", "Path not found: %s", path); return; } @@ -215,11 +195,7 @@ void cgit_print_blob(const char *hex, char *path, const char *head, int file_onl return; } - buf[size] = '\0'; - if (buffer_is_binary(buf, size)) - ctx.page.mimetype = "application/octet-stream"; - else - ctx.page.mimetype = "text/plain"; + ctx.page.mimetype = buffer_is_binary(buf, size) ? "application/octet-stream" : "text/plain"; ctx.page.filename = path; ctx.page.untrusted = 1; cgit_print_http_headers(); diff --git a/source/ui-blob.h b/source/ui-blob.h index 2c10c55..4f8597b 100644 --- a/source/ui-blob.h +++ b/source/ui-blob.h @@ -8,15 +8,15 @@ #ifndef CGIT_UI_BLOB_H #define CGIT_UI_BLOB_H -extern int cgit_ref_path_exists(const char *path, const char *ref, int file_only); +extern int cgit_ref_path_exists(const char *path, const char *ref); /* * Write the file at path in head, escaped when html_escape is set. The return * is 0 once it is written, -2 when it is past max-blob-size, and -1 when it * cannot be found or read. */ -extern int cgit_print_file(char *path, const char *head, int file_only, int html_escape); +extern int cgit_print_file(char *path, const char *head, int html_escape); -extern void cgit_print_blob(const char *hex, char *path, const char *head, int file_only); +extern void cgit_print_blob(const char *hex, char *path, const char *head); #endif // CGIT_UI_BLOB_H diff --git a/source/ui-clone.c b/source/ui-clone.c index dad21d5..bdfbdf7 100644 --- a/source/ui-clone.c +++ b/source/ui-clone.c @@ -38,19 +38,6 @@ static int print_ref(const struct reference *ref, void *cb_data UNUSED) } /* - * A path ending in a slash is handed back whole, where git's own - * pack_basename would return the empty string. - */ -static const char *last_path_component(const char *path) -{ - const char *slash = strrchr(path, '/'); - - if (slash && slash[1] != '\0') - return slash + 1; - return path; -} - -/* * Only the packs this repository holds itself are listed, because one * borrowed from an alternate is not reachable below this URL and a client * told about it would come back for a file that is not there. @@ -74,7 +61,7 @@ static void print_pack_info(void) struct packed_git *pack = entry->pack; if (pack->pack_local) - htmlf("P %s\n", last_path_component(pack->pack_name)); + htmlf("P %s\n", pack_basename(pack)); } } } @@ -169,8 +156,10 @@ void cgit_clone_objects(void) { char *path; - if (!ctx.qry.path) - goto err; + if (!ctx.qry.path) { + cgit_print_error_page(400, "Bad Request", "Bad request"); + return; + } if (!strcmp(ctx.qry.path, "info/packs")) { print_pack_info(); @@ -185,10 +174,6 @@ void cgit_clone_objects(void) path = repo_git_path(the_repository, "objects/%s", ctx.qry.path); send_file(path); free(path); - return; - -err: - cgit_print_error_page(400, "Bad Request", "Bad request"); } /* diff --git a/source/ui-commit.c b/source/ui-commit.c index 63f2b7e..0916c33 100644 --- a/source/ui-commit.c +++ b/source/ui-commit.c @@ -1,10 +1,9 @@ /* - * The commit page, which shows one commit on its own and is where the log and - * the ref listings link. It renders the idents, the object ids, the message, - * the trailer table when the repository asks for one and any note, then hands - * off to ui-diff.c for the diff against the first parent. Free text and - * addresses are written through the repository's commit, trailer and email - * filters, so a site can rewrite any of them on the way out. + * The commit page renders the idents, the object ids, the message, the trailer + * table when the repository asks for one and any note, then hands off to + * ui-diff.c for the diff against the first parent. Free text and addresses are + * written through the repository's commit, trailer and email filters, so a + * site can rewrite any of them on the way out. */ #define USE_THE_REPOSITORY_VARIABLE @@ -13,7 +12,6 @@ #include "filter.h" #include "html.h" #include "parsing.h" -#include "shared.h" #include "ui-commit.h" #include "ui-diff.h" #include "ui-log.h" @@ -32,18 +30,10 @@ static void print_ident_row(const char *role, const char *name, const char *emai timestamp_t date, int tz) { htmlf("<tr><th scope='row'>%s</th><td>", role); - cgit_open_filter(ctx.repo->email_filter, email ? email : "", "commit"); - html_txt(name); - if (ctx.cfg.enable_plain_email) { - html(" "); - html_txt(email); - } - cgit_close_filter(ctx.repo->email_filter); - html("</td><td class='right'><time datetime='"); - html_attr(show_date(date, tz, cgit_date_mode(DATE_ISO8601_STRICT))); - html("'>"); - html_txt(show_date(date, tz, cgit_date_mode(DATE_ISO8601))); - html("</time></td></tr>\n"); + cgit_print_person(name, email, "commit", 1); + html("</td><td class='right'>"); + cgit_print_date(date, tz); + html("</td></tr>\n"); } /* @@ -106,6 +96,7 @@ static void print_trailer_value(const char *key, const char *value) struct string_list *map; struct strbuf email = STRBUF_INIT; const char *name, *mail, *rest; + char *who; size_t name_len, mail_len; if (!split_ident_line(&ident, value, strlen(value)) && @@ -120,13 +111,9 @@ static void print_trailer_value(const char *key, const char *value) strbuf_addch(&email, '<'); strbuf_add(&email, mail, mail_len); strbuf_addch(&email, '>'); - cgit_open_filter(ctx.repo->email_filter, email.buf, "commit"); - html_ntxt(name, name_len); - if (ctx.cfg.enable_plain_email) { - html(" "); - html_txt(email.buf); - } - cgit_close_filter(ctx.repo->email_filter); + who = xmemdupz(name, name_len); + cgit_print_person(who, email.buf, "commit", 1); + free(who); strbuf_release(&email); rest = ident.mail_end + 1; while (isspace((unsigned char)*rest)) @@ -247,28 +234,21 @@ void cgit_print_commit(char *hex, const char *prefix) struct object_id oid, first_oid; const char *commit_hex; char *first_parent; - char *tree_rev; int parents; if (!hex) hex = ctx.qry.head; - if (repo_get_oid(the_repository, hex, &oid)) { - cgit_print_error_page(404, "Not Found", "Bad object id: %s", hex); + commit = cgit_get_commit(hex, &oid); + if (!commit) return; - } - commit = lookup_commit_reference(the_repository, &oid); - if (!commit) { - cgit_print_error_page(404, "Not Found", "Not a commit: %s", hex); - return; - } info = cgit_parse_commit(commit); format_display_notes(&commit->object.oid, ¬es, PAGE_ENCODING, 1); load_ref_decorations(NULL, DECORATE_FULL_REFS); - ctx.page.title = cgit_fmtalloc("%s - %s", info->subject, ctx.page.title); + ctx.page.title = xstrfmt("%s - %s", info->subject, ctx.page.title); cgit_print_layout_start(); // An octopus merge draws no diff, so it has no options to offer. if (commit_list_count(commit->parents) < OCTOPUS_PARENTS) @@ -293,13 +273,11 @@ void cgit_print_commit(char *hex, const char *prefix) parents = print_parent_rows(commit, hex, prefix, &first_oid); html("<tr><th scope='row'>tree</th><td colspan='2' class='oid'><div class='spanning'>"); - tree_rev = xstrdup(hex); - cgit_tree_link(oid_to_hex(get_commit_tree_oid(commit)), NULL, NULL, ctx.qry.head, tree_rev, NULL); + cgit_tree_link(oid_to_hex(get_commit_tree_oid(commit)), NULL, NULL, ctx.qry.head, hex, NULL); if (prefix) { html(" /"); - cgit_tree_link(prefix, NULL, NULL, ctx.qry.head, tree_rev, prefix); + cgit_tree_link(prefix, NULL, NULL, ctx.qry.head, hex, prefix); } - free(tree_rev); html("</div></td></tr>\n"); if (ctx.repo->snapshots) { diff --git a/source/ui-diff.c b/source/ui-diff.c index 91ad860..315a8f0 100644 --- a/source/ui-diff.c +++ b/source/ui-diff.c @@ -54,7 +54,6 @@ static int bodies_usable; static linediff_fn render_line_fn; static int render_suppressed; -static struct diff_filepair *current_filepair; static const char *current_prefix; static int use_ssdiff; @@ -187,15 +186,9 @@ static int show_filepair(struct diff_filepair *pair) starts_with(pair->two->path, current_prefix); } -/* - * The line xdiff produces ends in its newline, which is swapped for a - * terminator while the text is escaped and then put back, because the line - * points into a buffer xdiff still owns. - */ static void print_line(char *line, int len) { const char *class = "ctx"; - char c = line[len - 1]; if (line[0] == '+') class = "add"; @@ -205,31 +198,26 @@ static void print_line(char *line, int len) class = "hunk"; htmlf("<div class='%s'>", class); - line[len - 1] = '\0'; - html_txt(line); + html_ntxt(line, len - 1); html("</div>"); - line[len - 1] = c; } /* - * The buffer repo_find_unique_abbrev returns is reused, so a caller printing - * two abbreviated names together keeps a copy of each. + * Opens the group end_file closes, in whichever view the page uses, and writes + * the header of the pair's diff into it. */ -static char *abbrev_oid(const struct object_id *oid) -{ - return xstrdup(repo_find_unique_abbrev(the_repository, oid, DEFAULT_ABBREV)); -} - -static void print_file_header(const struct object_id *old_oid, const char *old_path, unsigned old_mode, - const struct object_id *new_oid, const char *new_path, unsigned new_mode) +static void print_file_header(struct diff_filepair *pair) { - char *old_abbrev, *new_abbrev; + const struct object_id *old_oid = &pair->one->oid, *new_oid = &pair->two->oid; + const char *old_path = pair->one->path, *new_path = pair->two->path; + unsigned old_mode = pair->one->mode, new_mode = pair->two->mode; + char old_abbrev[GIT_MAX_HEXSZ + 1], new_abbrev[GIT_MAX_HEXSZ + 1]; int subproject; subproject = (S_ISGITLINK(old_mode) || S_ISGITLINK(new_mode)); - // The side by side view opens its file group before the header row, - // so only the unified view opens one here. - if (!use_ssdiff) { + if (use_ssdiff) { + cgit_ssdiff_header_begin(pair); + } else { html("<div class='file' data-path='"); html_attr(new_path); html("'>"); @@ -247,11 +235,9 @@ static void print_file_header(const struct object_id *old_oid, const char *old_p htmlf("<br>deleted file mode %.6o", old_mode); if (!subproject) { - old_abbrev = abbrev_oid(old_oid); - new_abbrev = abbrev_oid(new_oid); + repo_find_unique_abbrev_r(the_repository, old_abbrev, old_oid, DEFAULT_ABBREV); + repo_find_unique_abbrev_r(the_repository, new_abbrev, new_oid, DEFAULT_ABBREV); htmlf("<br>index %s..%s", old_abbrev, new_abbrev); - free(old_abbrev); - free(new_abbrev); if (old_mode != 0 && new_mode != 0) { htmlf(" %.6o", old_mode); if (new_mode != old_mode) @@ -279,12 +265,13 @@ static void print_file_header(const struct object_id *old_oid, const char *old_p html_txt(new_path); } html("</div>"); + if (use_ssdiff) + cgit_ssdiff_header_end(); } /* - * Closes the group print_file_header or cgit_ssdiff_header_begin opened. The - * side by side footer closes its own, so the unified view is the only one with - * work to do here. + * Closes the group print_file_header opened. The side by side footer closes its + * own, so the unified view is the only one with work to do here. */ static void end_file(void) { @@ -338,17 +325,13 @@ static void print_truncated(const char *path) static struct fileinfo *reserve_item(void) { - files++; - if (files >= slots) { - if (slots == 0) - slots = 4; - else - slots = slots * 2; - items = xrealloc(items, slots * sizeof(struct fileinfo)); - } - memset(&items[files - 1], 0, sizeof(items[files - 1])); - strbuf_init(&items[files - 1].body, 0); - return &items[files - 1]; + struct fileinfo *item; + + ALLOC_GROW(items, files + 1, slots); + item = &items[files++]; + memset(item, 0, sizeof(*item)); + strbuf_init(&item->body, 0); + return item; } /* @@ -362,15 +345,8 @@ static void collect_body(struct diff_filepair *pair, struct fileinfo *item, int linediff_fn line_fn = use_ssdiff ? cgit_ssdiff_line_cb : print_line; size_t header_len; - current_filepair = pair; - html_capture_begin(body); - if (use_ssdiff) - cgit_ssdiff_header_begin(pair->two->path); - print_file_header(&pair->one->oid, pair->one->path, pair->one->mode, - &pair->two->oid, pair->two->path, pair->two->mode); - if (use_ssdiff) - cgit_ssdiff_header_end(); + print_file_header(pair); header_len = body->len; // Cleared for the whole file rather than in the branch below, because @@ -468,7 +444,6 @@ static void print_diffstat(const struct object_id *old_oid, const struct object_ html("')"); } html("</div>\n"); - max_changes = 0; cgit_diff_tree(old_oid, new_oid, inspect_filepair, prefix, ctx.qry.ignorews); // A change to no files would leave the table an empty frame. if (files) { @@ -498,21 +473,13 @@ static void filepair_cb(struct diff_filepair *pair) size_t new_size = 0; int binary = 0; int idx; - linediff_fn line_fn = print_line; + linediff_fn line_fn = use_ssdiff ? cgit_ssdiff_line_cb : print_line; if (!show_filepair(pair)) return; idx = item_idx++; - current_filepair = pair; - if (use_ssdiff) { - cgit_ssdiff_header_begin(pair->two->path); - line_fn = cgit_ssdiff_line_cb; - } - print_file_header(&pair->one->oid, pair->one->path, pair->one->mode, - &pair->two->oid, pair->two->path, pair->two->mode); - if (use_ssdiff) - cgit_ssdiff_header_end(); + print_file_header(pair); if (over_line_cap(idx)) { print_truncated(items[idx].new_path); @@ -557,16 +524,6 @@ static void print_raw_patch(const struct object_id *old_tree_oid, const struct o diff_flush(&diffopt); } -struct diff_filespec *cgit_get_current_old_file(void) -{ - return current_filepair->one; -} - -struct diff_filespec *cgit_get_current_new_file(void) -{ - return current_filepair->two; -} - void cgit_print_diff_ctrls(void) { int i, selected; @@ -574,7 +531,7 @@ void cgit_print_diff_ctrls(void) html("<div class='cgit-panel'>\n"); html("<strong>Diff options</strong>"); html("<form method='get' aria-label='Diff options'>"); - cgit_add_hidden_formfields(1, 0, ctx.qry.page); + cgit_add_hidden_formfields(1, 0); html("<div class='fields'>"); html("<label for='diff-context'>context</label>"); html("<select id='diff-context' name='context'>"); @@ -647,31 +604,17 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefi if (!new_rev) new_rev = ctx.qry.head; - if (repo_get_oid(the_repository, new_rev, new_rev_oid)) { - cgit_print_error_page(404, "Not Found", "Bad object id: %s", new_rev); - return; - } - new_commit = lookup_commit_reference(the_repository, new_rev_oid); - if (!new_commit || repo_parse_commit(the_repository, new_commit)) { - cgit_print_error_page(404, "Not Found", "Not a commit: %s", oid_to_hex(new_rev_oid)); + new_commit = cgit_get_commit(new_rev, new_rev_oid); + if (!new_commit || cgit_get_old_oid(old_rev, new_commit, old_rev_oid)) return; - } new_tree_oid = get_commit_tree_oid(new_commit); - if (old_rev) { - if (repo_get_oid(the_repository, old_rev, old_rev_oid)) { - cgit_print_error_page(404, "Not Found", "Bad object id: %s", old_rev); - return; - } - } else if (new_commit->parents && new_commit->parents->item) { - oidcpy(old_rev_oid, &new_commit->parents->item->object.oid); - } else { - oidclr(old_rev_oid, the_repository->hash_algo); - } - if (!is_null_oid(old_rev_oid)) { + // A named old side is a commit already, so only a first parent + // missing from the repository fails here, and it has no name + // but its id. old_commit = lookup_commit_reference(the_repository, old_rev_oid); - if (!old_commit || repo_parse_commit(the_repository, old_commit)) { + if (!old_commit) { cgit_print_error_page(404, "Not Found", "Not a commit: %s", oid_to_hex(old_rev_oid)); return; } @@ -742,7 +685,6 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefi html_raw(items[i].body.buf, items[i].body.len); release_bodies(); } else { - item_idx = 0; cgit_diff_tree(old_rev_oid, new_rev_oid, filepair_cb, prefix, ctx.qry.ignorews); } if (!use_ssdiff) diff --git a/source/ui-diff.h b/source/ui-diff.h index 467c79f..359fad2 100644 --- a/source/ui-diff.h +++ b/source/ui-diff.h @@ -1,9 +1,8 @@ /* * The page that shows what one revision changed against another, and the * options panel that sits above it. The side by side renderer in ui-ssdiff - * builds a link on every line number, so the two revisions being compared and - * the file pair being rendered are published here for it rather than kept - * private to the page. + * builds a link on every line number, so the two revisions being compared are + * published here for it rather than kept private to the page. */ #ifndef CGIT_UI_DIFF_H @@ -17,12 +16,9 @@ extern void cgit_print_diff_ctrls(void); -extern void cgit_print_diff(const char *new_hex, const char *old_hex, const char *prefix, +extern void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefix, int show_ctrls, int raw); -extern struct diff_filespec *cgit_get_current_old_file(void); -extern struct diff_filespec *cgit_get_current_new_file(void); - extern struct object_id old_rev_oid[1]; extern struct object_id new_rev_oid[1]; diff --git a/source/ui-empty.c b/source/ui-empty.c index deefc21..76b2697 100644 --- a/source/ui-empty.c +++ b/source/ui-empty.c @@ -11,8 +11,17 @@ #include "ui-empty.h" #include "ui-shared.h" +static int clone_urls_printed; + static void print_clone_url(const char *url) { + // cgit_add_clone_urls may call back no times at all, so the table + // waits for a first url rather than being opened ahead of the walk. + if (clone_urls_printed++ == 0) { + html("<table class='list clone' aria-label='Clone urls'>\n<tbody class='clone'>\n"); + html("<tr class='nohover'><th class='left' scope='col'>Clone</th></tr>\n"); + } + cgit_print_clone_row(url, 0); } @@ -23,14 +32,8 @@ static void print_clone_url(const char *url) void cgit_print_empty_repo(void) { cgit_print_error("Repository seems to be empty"); - - // cgit_add_clone_urls draws on these two alone, so with neither set - // the table below would be a Clone heading with no rows. - if (!ctx.repo->clone_url && !ctx.cfg.clone_prefix) - return; - - html("<table class='list clone' aria-label='Clone urls'>\n<tbody class='clone'>\n"); - html("<tr class='nohover'><th class='left' scope='col'>Clone</th></tr>\n"); + clone_urls_printed = 0; cgit_add_clone_urls(print_clone_url); - html("</tbody>\n</table>\n"); + if (clone_urls_printed) + html("</tbody>\n</table>\n"); } diff --git a/source/ui-log.c b/source/ui-log.c index 870ff06..5b3a88a 100644 --- a/source/ui-log.c +++ b/source/ui-log.c @@ -11,7 +11,6 @@ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" -#include "filter.h" #include "html.h" #include "parsing.h" #include "shared.h" @@ -212,28 +211,26 @@ static void print_graph_padding(struct rev_info *revs, struct strbuf *graph, int html("</td>\n"); } -// Ends a row in a table wider than the log with an empty cell for each column -// the log has no use for. +/* + * Ends a row in a table wider than the log with an empty cell for each column + * the log has no use for. + */ static void print_filler(int columns) { while (columns-- > 0) html("<td></td>"); } -static void print_commit(struct commit *commit, struct rev_info *revs, int filler) +/* + * columns counts the filler too, because the rows that span the table reach + * across it as well. + */ +static void print_commit(struct commit *commit, struct rev_info *revs, int columns, int filler) { struct commitinfo *info; - int columns = revs->graph ? 4 : 3; struct strbuf graphbuf = STRBUF_INIT; struct strbuf msgbuf = STRBUF_INIT; - if (ctx.repo->enable_log_filecount) - columns++; - if (ctx.repo->enable_log_linecount) - columns++; - // The rows that span the table reach across the filler as well. - columns += filler; - if (revs->graph) print_graph_above(revs, &graphbuf, columns); @@ -258,9 +255,7 @@ static void print_commit(struct commit *commit, struct rev_info *revs, int fille oid_to_hex(&commit->object.oid), ctx.qry.vpath); cgit_print_commit_decorations(commit); html("</td><td class='col-author'>"); - cgit_open_filter(ctx.repo->email_filter, info->author_email ? info->author_email : "", "log"); - html_txt(info->author); - cgit_close_filter(ctx.repo->email_filter); + cgit_print_person(info->author, info->author_email, "log", 0); if (revs->graph) { html("</td><td>"); @@ -355,28 +350,6 @@ static int valid_range_token(const char *arg) return ok; } -static char *next_token(char **src) -{ - char *token; - - if (!*src) - return NULL; - while (isspace((unsigned char)**src)) - (*src)++; - if (!**src) - return NULL; - token = *src; - while (**src) { - if (isspace((unsigned char)**src)) { - **src = '\0'; - (*src)++; - break; - } - (*src)++; - } - return token; -} - /* * Whether there is a page after this one is answered by pulling one more * commit off the walk. The links carry the path as it was asked for, since a @@ -466,13 +439,25 @@ void cgit_print_commit_decorations(struct commit *commit) html("</span>"); } -void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern, +int cgit_log_columns(int graph) +{ + int columns = graph ? 4 : 3; + + if (ctx.repo->enable_log_filecount) + columns++; + if (ctx.repo->enable_log_linecount) + columns++; + return columns; +} + +void cgit_print_log(const char *tip, int ofs, int cnt, const char *grep, const char *pattern, const char *path, int pager, int commit_graph, int commit_sort, int table_columns) { - struct rev_info rev; + struct rev_info rev = REV_INFO_INIT; struct commit *commit; struct strvec rev_argv = STRVEC_INIT; - int i, columns = commit_graph ? 4 : 3, filler = 0; + struct string_list tokens = STRING_LIST_INIT_DUP; + int i, columns, filler = 0; int must_free_tip = 0; // setup_revisions skips the first entry the way a command skips its @@ -487,25 +472,14 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern // option. if (tip && !cgit_valid_rev(tip)) { cgit_print_error_page(400, "Bad Request", "Invalid revision"); - if (must_free_tip) - free((char *)tip); - strvec_clear(&rev_argv); - return; + goto out; } strvec_push(&rev_argv, tip); - // A leading colon makes git read the path as a pathspec with magic, - // which can end the request inside git. - if (path && path[0] == ':') { - cgit_print_error_page(400, "Bad Request", "Invalid path"); - if (must_free_tip) - free((char *)tip); - strvec_clear(&rev_argv); - return; - } + if (cgit_refuse_pathspec_magic(path)) + goto out; if (grep && pattern && *pattern) { - pattern = xstrdup(pattern); if (!strcmp(grep, "grep") || !strcmp(grep, "author") || !strcmp(grep, "committer")) { // The search is literal and ignores case. setup_revisions // compiles the pattern, so the flags go in with it. @@ -513,21 +487,19 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern strvec_push(&rev_argv, "--regexp-ignore-case"); strvec_pushf(&rev_argv, "--%s=%s", grep, pattern); } else if (!strcmp(grep, "range")) { - char *arg; + struct string_list_item *token; // Each whitespace separated token has to pass the same // check as any other revision. The tip pushed above goes // away, since the range supersedes it. strvec_pop(&rev_argv); - while ((arg = next_token(&pattern))) { - if (!valid_range_token(arg)) { + string_list_split_f(&tokens, pattern, " \t\n\r", -1, STRING_LIST_SPLIT_NONEMPTY); + for_each_string_list_item(token, &tokens) { + if (!valid_range_token(token->string)) { cgit_print_error_page(400, "Bad Request", "Invalid revision"); - if (must_free_tip) - free((char *)tip); - strvec_clear(&rev_argv); - return; + goto out; } - strvec_push(&rev_argv, arg); + strvec_push(&rev_argv, token->string); } } } @@ -555,28 +527,24 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern repo_init_revisions(the_repository, &rev, NULL); // The author and committer searches then match the mapped idents. rev.mailmap = cgit_mailmap(); - rev.abbrev = DEFAULT_ABBREV; - rev.commit_format = CMIT_FMT_DEFAULT; - rev.verbose_header = 1; - rev.show_root_diff = 0; rev.ignore_missing = 1; - rev.simplify_history = 1; - setup_revisions(rev_argv.nr, rev_argv.v, &rev, NULL); + setup_revisions_from_strvec(&rev_argv, &rev, NULL); load_ref_decorations(NULL, DECORATE_FULL_REFS); rev.show_decorations = 1; + // A followed log is walked without the graph even when commit_graph + // asks for one, so the walk decides the columns. + columns = cgit_log_columns(rev.graph != NULL); + if (table_columns > columns) + filler = table_columns - columns; + rev.diffopt.detect_rename = 1; rev.diffopt.rename_limit = ctx.cfg.renamelimit; if (ctx.qry.ignorews) DIFF_XDL_SET(&rev.diffopt, IGNORE_WHITESPACE); - // A failed setup leaves the walk holding freed commits, so it must - // not be read from. - if (prepare_revision_walk(&rev)) { - cgit_print_error_page(500, "Internal Server Error", "Unable to read the history"); - strvec_clear(&rev_argv); - return; - } + if (cgit_prepare_walk(&rev)) + goto out; if (pager) { cgit_print_layout_start(); @@ -584,7 +552,7 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern } html("<tbody class='log'>\n<tr class='nohover'>"); - if (commit_graph) + if (rev.graph) html("<th scope='col' aria-label='Graph'></th>"); else html("<th class='left' scope='col'>Age</th>"); @@ -601,16 +569,10 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern html("</th><th class='left col-author' scope='col'>Author</th>"); if (rev.graph) html("<th class='left' scope='col'>Age</th>"); - if (ctx.repo->enable_log_filecount) { + if (ctx.repo->enable_log_filecount) html("<th class='left col-count' scope='col'>Files</th>"); - columns++; - } - if (ctx.repo->enable_log_linecount) { + if (ctx.repo->enable_log_linecount) html("<th class='left col-count' scope='col'>Lines</th>"); - columns++; - } - if (table_columns > columns) - filler = table_columns - columns; print_filler(filler); html("</tr>\n"); @@ -630,7 +592,7 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern counts_ready = 0; if (should_show(commit, &rev)) { i++; - print_commit(commit, &rev, filler); + print_commit(commit, &rev, columns + filler, filler); } release_commit_memory(the_repository->parsed_objects, commit); commit->parents = NULL; @@ -650,6 +612,12 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern html("</tbody>\n"); } + // The mailmap is cgit's own and outlives the walk. + rev.mailmap = NULL; + release_revisions(&rev); +out: if (must_free_tip) free((char *)tip); + strvec_clear(&rev_argv); + string_list_clear(&tokens, 0); } diff --git a/source/ui-log.h b/source/ui-log.h index d01a63d..349489a 100644 --- a/source/ui-log.h +++ b/source/ui-log.h @@ -14,8 +14,15 @@ * is that table's width, which every row is filled out to. 0 leaves the rows * at their own width. */ -extern void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern, +extern void cgit_print_log(const char *tip, int ofs, int cnt, const char *grep, const char *pattern, const char *path, int pager, int commit_graph, int commit_sort, int table_columns); + +/* + * The columns a log row spans: age, message and author, and one more each for + * the graph and for the file and line counts that are turned on. + */ +extern int cgit_log_columns(int graph); + extern void cgit_print_commit_decorations(struct commit *commit); #endif // CGIT_UI_LOG_H diff --git a/source/ui-patch.c b/source/ui-patch.c index 04083fe..0fa9a2e 100644 --- a/source/ui-patch.c +++ b/source/ui-patch.c @@ -10,100 +10,52 @@ #include "cgit.h" #include "html.h" -#include "ui-diff.h" #include "ui-patch.h" #include "ui-shared.h" // Room for two hex object ids, the two dots between them, and the null byte. #define REV_RANGE_LEN (2 * GIT_MAX_HEXSZ + 3) -// Where the format argument sits in the walk arguments below. -#define FORMAT_ARG 2 - -/* - * A root commit has no parent, so the old end is left null and the caller asks - * for that one commit instead. - */ -static int resolve_range(const char *new_rev, const char *old_rev, - struct object_id *new_oid, struct object_id *old_oid) -{ - struct commit *commit; - - if (repo_get_oid(the_repository, new_rev, new_oid)) { - cgit_print_error_page(404, "Not Found", "Bad object id: %s", new_rev); - return -1; - } - commit = lookup_commit_reference(the_repository, new_oid); - if (!commit) { - cgit_print_error_page(404, "Not Found", "Not a commit: %s", new_rev); - return -1; - } - - if (old_rev) { - if (repo_get_oid(the_repository, old_rev, old_oid)) { - cgit_print_error_page(404, "Not Found", "Bad object id: %s", old_rev); - return -1; - } - if (!lookup_commit_reference(the_repository, old_oid)) { - cgit_print_error_page(404, "Not Found", "Not a commit: %s", old_rev); - return -1; - } - } else if (commit->parents && commit->parents->item) { - oidcpy(old_oid, &commit->parents->item->object.oid); - } else { - oidclr(old_oid, the_repository->hash_algo); - } - return 0; -} - void cgit_print_patch(const char *new_rev, const char *old_rev, const char *prefix) { struct rev_info rev; struct commit *commit; struct object_id new_oid, old_oid; + struct strvec rev_argv = STRVEC_INIT; char rev_range[REV_RANGE_LEN]; const char *version; - // setup_revisions reads these the way git reads a command line, so the - // first entry stands in for the program name and is skipped, and the - // array has to stay null terminated because the path after the double - // dash is picked up past the count. - const char *rev_argv[] = { NULL, "--reverse", "--format=email", rev_range, "--", prefix, NULL }; - int rev_argc = ARRAY_SIZE(rev_argv) - 1; - - if (!prefix) - rev_argc--; if (!new_rev) new_rev = ctx.qry.head; - // A leading colon makes git read the path as a pathspec with magic, - // which can end the request inside git after the headers are out. - if (prefix && prefix[0] == ':') { - cgit_print_error_page(400, "Bad Request", "Invalid path"); + if (cgit_refuse_pathspec_magic(prefix)) return; - } - if (resolve_range(new_rev, old_rev, &new_oid, &old_oid)) + commit = cgit_get_commit(new_rev, &new_oid); + if (!commit || cgit_get_old_oid(old_rev, commit, &old_oid)) return; if (is_null_oid(&old_oid)) { - memcpy(rev_range, oid_to_hex(&new_oid), the_hash_algo->hexsz + 1); + oid_to_hex_r(rev_range, &new_oid); } else { - xsnprintf(rev_range, REV_RANGE_LEN, "%s..%s", oid_to_hex(&old_oid), oid_to_hex(&new_oid)); + xsnprintf(rev_range, sizeof(rev_range), "%s..%s", oid_to_hex(&old_oid), oid_to_hex(&new_oid)); } - if (!ctx.cfg.enable_plain_email) { - rev_argv[FORMAT_ARG] = - "--format=format:From %H Mon Sep 17 00:00:00 2001%n" - "From: %an%nDate: %aD%n%w(78,0,1)Subject: %s%n%n%w(0)%b"; - } + strvec_pushl(&rev_argv, "patch", "--reverse", NULL); + if (ctx.cfg.enable_plain_email) + strvec_push(&rev_argv, "--format=email"); + else + strvec_push(&rev_argv, "--format=format:From %H Mon Sep 17 00:00:00 2001%n" + "From: %an%nDate: %aD%n%w(78,0,1)Subject: %s%n%n%w(0)%b"); + strvec_pushl(&rev_argv, rev_range, "--", NULL); + if (prefix) + strvec_push(&rev_argv, prefix); // Git inlines a blob up to its own big-file threshold, far past what // any other view inflates, so max-blob-size is applied through it. if (ctx.cfg.max_blob_size) repo_settings_set_big_file_threshold(the_repository, (unsigned long)ctx.cfg.max_blob_size * 1024); repo_init_revisions(the_repository, &rev, NULL); - rev.abbrev = DEFAULT_ABBREV; rev.verbose_header = 1; rev.diff = 1; rev.show_root_diff = 1; @@ -113,17 +65,16 @@ void cgit_print_patch(const char *new_rev, const char *old_rev, const char *pref // the path comes from the request and a long one would abort the // process. if (prefix) - rev.diffopt.stat_sep = cgit_fmtalloc("(limited to '%s')\n\n", prefix); - setup_revisions(rev_argc, rev_argv, &rev, NULL); + rev.diffopt.stat_sep = xstrfmt("(limited to '%s')\n\n", prefix); + setup_revisions_from_strvec(&rev_argv, &rev, NULL); // A single commit resolves to a range starting at its parent, so this // only ever cuts an explicit range short and keeps one request from // emitting a patch for the whole history. if (ctx.cfg.max_patch_count > 0) rev.max_count = ctx.cfg.max_patch_count; - // A failed setup leaves the walk holding freed commits, so it must - // not be read from, and the headers wait until it has succeeded. - if (prepare_revision_walk(&rev)) { - cgit_print_error_page(500, "Internal Server Error", "Unable to read the history"); + // The headers wait until the walk has started. + if (cgit_prepare_walk(&rev)) { + strvec_clear(&rev_argv); return; } @@ -132,8 +83,7 @@ void cgit_print_patch(const char *new_rev, const char *old_rev, const char *pref cgit_print_http_headers(); // git format-patch signs with its bare version number, so the v that - // cgit's tags carry is left off to read the same way. Without - // enable-version-info the signature names cgit alone. + // cgit's tags carry is left off to read the same way. if (!skip_prefix(cgit_version, "v", &version)) version = cgit_version; @@ -147,4 +97,6 @@ void cgit_print_patch(const char *new_rev, const char *old_rev, const char *pref else printf("-- \ncgit\n\n"); } + release_revisions(&rev); + strvec_clear(&rev_argv); } diff --git a/source/ui-plain.c b/source/ui-plain.c index 57e4699..43bdb22 100644 --- a/source/ui-plain.c +++ b/source/ui-plain.c @@ -42,6 +42,55 @@ static int is_unsafe_type(const char *mimetype) strcmp(mimetype, "application/pdf"); } +static char *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)) { + // 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); + if (!fields.nr || fields.items[0].string[0] == '#') { + string_list_clear(&fields, 0); + continue; + } + 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; +} + /* * Writes the response for the object, error pages included, so the walk * does not go on to report the path as missing. @@ -69,7 +118,7 @@ static void print_object(const struct object_id *oid, const char *path) return; } - mimetype = cgit_get_mimetype_for_filename(path); + mimetype = get_mimetype_for_filename(path); ctx.page.mimetype = mimetype; if (!ctx.repo->enable_html_serving) { @@ -93,9 +142,9 @@ static void print_object(const struct object_id *oid, const char *path) static char *build_path(const char *base, int baselen, const char *path) { if (path[0]) - return cgit_fmtalloc("%.*s%s/", baselen, base, path); + return xstrfmt("%.*s%s/", baselen, base, path); else - return cgit_fmtalloc("%.*s/", baselen, base); + return xstrfmt("%.*s/", baselen, base); } static void print_dir(const char *base, int baselen, const char *path) @@ -159,20 +208,17 @@ static void print_dir_tail(void) html("</ul>\n</body>\n</html>\n"); } -/* - * read_tree reads the return value as a direction rather than a status, so - * READ_TREE_RECURSIVE means step into this entry and zero means step over it. - */ static int walk_tree(const struct object_id *oid, struct strbuf *base, const char *pathname, unsigned mode, void *context) { struct walk_tree_context *walk = context; - // A tree can hold two entries of one name, and only the first answers: - // a second response would follow the first in the same body. - if (walk->response != RESPONSE_NONE && walk->dir_len >= 0 && base->len == (size_t)walk->dir_len) - return 0; if (walk->dir_len >= 0 && base->len == (size_t)walk->dir_len) { + // A tree can hold two entries of one name, and only the first + // answers: a second response would follow the first in the same + // body. + if (walk->response != RESPONSE_NONE) + return 0; if (S_ISREG(mode) || S_ISLNK(mode)) { print_object(oid, pathname); walk->response = RESPONSE_BLOB; @@ -205,19 +251,8 @@ void cgit_print_plain(void) const char *rev = ctx.qry.oid; struct object_id oid; struct commit *commit; - int path_len = ctx.qry.path ? strlen(ctx.qry.path) : 0; - // nowildcard_len matches len so git treats the path as literal. As a - // glob, every entry a pattern like * matches would be answered with - // its own HTTP headers inside the body of the first. - struct pathspec_item path_items = { - .match = ctx.qry.path, - .len = path_len, - .nowildcard_len = path_len - }; - struct pathspec paths = { - .nr = 1, - .items = &path_items - }; + struct pathspec_item path_item; + struct pathspec paths = { 0 }; struct walk_tree_context walk = { .response = RESPONSE_NONE }; @@ -234,15 +269,18 @@ void cgit_print_plain(void) cgit_print_error_page(404, "Not Found", "Not found"); return; } - if (!path_items.match) { + // The path is taken literally, since as a glob every entry a pattern + // like * matches would be answered with its own HTTP headers inside + // the body of the first. + cgit_literal_pathspec(&paths, &path_item, ctx.qry.path); + if (!ctx.qry.path) { // The walk is never handed an entry for the top of the tree // itself, so the listing it would have opened is opened here. - paths.nr = 0; walk.dir_len = -1; print_dir("", 0, ""); walk.response = RESPONSE_LISTING; } else { - walk.dir_len = dir_prefix_len(path_items.match); + walk.dir_len = dir_prefix_len(ctx.qry.path); } read_tree(the_repository, repo_get_commit_tree(the_repository, commit), &paths, walk_tree, &walk); if (walk.response == RESPONSE_NONE) diff --git a/source/ui-refs.c b/source/ui-refs.c index c8e8f3f..69a2cc1 100644 --- a/source/ui-refs.c +++ b/source/ui-refs.c @@ -3,19 +3,33 @@ * the whole of a repository's refs page. Every row is one ref beside * the commit or tag object it points at, ordered newest first, with branches * then reordered by name unless branch-sort asks for age. A section longer - * than max-ref-count is cut short and ends in a link to a dedicated heads or + * than its cap is cut short and ends in a link to a dedicated heads or * tags page, which walks the same list one offset at a time. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" -#include "filter.h" #include "html.h" -#include "shared.h" +#include "parsing.h" #include "ui-refs.h" #include "ui-shared.h" +struct refinfo { + const char *refname; + struct object *object; + union { + struct taginfo *tag; + struct commitinfo *commit; + }; +}; + +struct reflist { + struct refinfo **refs; + int alloc; + int count; +}; + // The slice of a sorted ref list that one page shows, with end one past the // last row. size is what a full page holds, so it also decides whether the // list needs a pager. @@ -25,6 +39,68 @@ struct ref_page { int end; }; +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 void free_reflist_inner(struct reflist *list) +{ + int i; + + for (i = 0; i < list->count; i++) + free_refinfo(list->refs[i]); + free(list->refs); +} + +/* + * Matches git's reference iteration callback, and appends each ref it is given + * to the struct reflist passed as cb_data. + */ +static int 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) { + ALLOC_GROW(list->refs, list->count + 1, list->alloc); + list->refs[list->count++] = info; + } + return 0; +} + /* * The tagger date and the committer date live in a union in struct refinfo and * only the member matching the object type is ever filled, so assuming a ref @@ -32,13 +108,11 @@ struct ref_page { */ static timestamp_t ref_date(struct refinfo *ref) { - if (!ref->object) - return 0; switch (ref->object->type) { case OBJ_TAG: return ref->tag ? ref->tag->tagger_date : 0; case OBJ_COMMIT: - return ref->commit ? ref->commit->committer_date : 0; + return ref->commit->committer_date; } return 0; } @@ -71,9 +145,9 @@ static void collect_branches(struct reflist *list) { list->refs = NULL; list->alloc = list->count = 0; - refs_for_each_branch_ref(get_main_ref_store(the_repository), cgit_refs_cb, list); + refs_for_each_branch_ref(get_main_ref_store(the_repository), refs_cb, list); if (ctx.repo->enable_remote_branches) - refs_for_each_remote_ref(get_main_ref_store(the_repository), cgit_refs_cb, list); + refs_for_each_remote_ref(get_main_ref_store(the_repository), refs_cb, list); } /* @@ -99,13 +173,13 @@ static void print_branch_header(int columns) htmlf("<th%s class='left' scope='col'>Age</th></tr>\n", age_colspan(columns)); } -static int print_branch(struct refinfo *ref, int columns) +static void print_branch(struct refinfo *ref, int columns) { struct commitinfo *info = ref->commit; const char *name = ref->refname; if (!info) - return 1; + return; html("<tr class='branch'><td>"); cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL, ctx.qry.showmsg, 0); html("</td><td>"); @@ -113,12 +187,7 @@ static int print_branch(struct refinfo *ref, int columns) if (ref->object->type == OBJ_COMMIT) { cgit_commit_link(info->subject, NULL, NULL, name, NULL, NULL); html("</td><td class='col-author'>"); - // A filter must not be handed a NULL argument. - cgit_open_filter( - ctx.repo->email_filter, info->author_email ? info->author_email : "", "refs" - ); - html_txt(info->author); - cgit_close_filter(ctx.repo->email_filter); + cgit_print_person(info->author, info->author_email, "refs", 0); htmlf("</td><td%s>", age_colspan(columns)); cgit_print_age(info->committer_date, info->committer_tz, -1); } else { @@ -128,14 +197,13 @@ static int print_branch(struct refinfo *ref, int columns) htmlf("</td><td class='col-author'></td><td%s>", age_colspan(columns)); } html("</td></tr>\n"); - return 0; } static void collect_tags(struct reflist *list) { list->refs = NULL; list->alloc = list->count = 0; - refs_for_each_tag_ref(get_main_ref_store(the_repository), cgit_refs_cb, list); + refs_for_each_tag_ref(get_main_ref_store(the_repository), refs_cb, list); } static void print_tag_header(int columns) @@ -149,7 +217,7 @@ static void print_tag_header(int columns) htmlf("<th%s class='left' scope='col'>Age</th></tr>\n", age_colspan(columns)); } -static int print_tag(struct refinfo *ref, int columns) +static void print_tag(struct refinfo *ref, int columns) { struct taginfo *info = NULL; const char *name = ref->refname; @@ -163,7 +231,7 @@ static int print_tag(struct refinfo *ref, int columns) obj = tag->tagged; info = ref->tag; if (!info) - return 1; + return; } html("<tr class='tag'><td>"); @@ -175,18 +243,10 @@ static int print_tag(struct refinfo *ref, int columns) cgit_object_link(obj, 0); html("</td><td class='col-author'>"); if (info) { - if (info->tagger) { - cgit_open_filter(ctx.repo->email_filter, info->tagger_email, "refs"); - html_txt(info->tagger); - cgit_close_filter(ctx.repo->email_filter); - } + if (info->tagger) + cgit_print_person(info->tagger, info->tagger_email, "refs", 0); } else if (ref->object->type == OBJ_COMMIT) { - cgit_open_filter( - ctx.repo->email_filter, ref->commit->author_email ? ref->commit->author_email : "", - "refs" - ); - html_txt(ref->commit->author); - cgit_close_filter(ctx.repo->email_filter); + cgit_print_person(ref->commit->author, ref->commit->author_email, "refs", 0); } htmlf("</td><td%s>", age_colspan(columns)); if (info) { @@ -196,8 +256,6 @@ static int print_tag(struct refinfo *ref, int columns) cgit_print_age(ref->commit->commit->date, 0, -1); } html("</td></tr>\n"); - - return 0; } static void print_refs_link(const char *path, int columns) @@ -258,9 +316,9 @@ static void print_branches_page(int pagesize) print_branch_header(REF_COLUMNS); collect_branches(&list); - cgit_qsort(list.refs, list.count, sizeof(*list.refs), cmp_date); + QSORT(list.refs, list.count, cmp_date); if (ctx.repo->branch_sort == 0) - cgit_qsort(list.refs, list.count, sizeof(*list.refs), cmp_name); + QSORT(list.refs, list.count, cmp_name); page = page_bounds(pagesize, list.count); @@ -271,7 +329,7 @@ static void print_branches_page(int pagesize) print_ref_pager(page.start, page.size, list.count, "heads"); html("</tbody>\n"); - cgit_free_reflist_inner(&list); + free_reflist_inner(&list); } static void print_tags_page(int pagesize) @@ -283,7 +341,7 @@ static void print_tags_page(int pagesize) collect_tags(&list); if (list.count == 0) return; - cgit_qsort(list.refs, list.count, sizeof(*list.refs), cmp_date); + QSORT(list.refs, list.count, cmp_date); page = page_bounds(pagesize, list.count); @@ -295,7 +353,7 @@ static void print_tags_page(int pagesize) print_ref_pager(page.start, page.size, list.count, "tags"); html("</tbody>\n"); - cgit_free_reflist_inner(&list); + free_reflist_inner(&list); } void cgit_print_branches(int maxcount, int columns) @@ -312,9 +370,9 @@ void cgit_print_branches(int maxcount, int columns) // The date sort covers the list, the name sort only the rows about to // be shown, so the section holds the newest branches rather than the // first ones by name. - cgit_qsort(list.refs, list.count, sizeof(*list.refs), cmp_date); + QSORT(list.refs, list.count, cmp_date); if (ctx.repo->branch_sort == 0) - cgit_qsort(list.refs, maxcount, sizeof(*list.refs), cmp_name); + QSORT(list.refs, maxcount, cmp_name); for (i = 0; i < maxcount; i++) print_branch(list.refs[i], columns); @@ -323,7 +381,7 @@ void cgit_print_branches(int maxcount, int columns) print_refs_link("heads", columns); html("</tbody>\n"); - cgit_free_reflist_inner(&list); + free_reflist_inner(&list); } void cgit_print_tags(int maxcount, int columns) @@ -334,7 +392,7 @@ void cgit_print_tags(int maxcount, int columns) collect_tags(&list); if (list.count == 0) return; - cgit_qsort(list.refs, list.count, sizeof(*list.refs), cmp_date); + QSORT(list.refs, list.count, cmp_date); if (maxcount <= 0 || maxcount > list.count) maxcount = list.count; print_tag_header(columns); @@ -345,7 +403,7 @@ void cgit_print_tags(int maxcount, int columns) print_refs_link("tags", columns); html("</tbody>\n"); - cgit_free_reflist_inner(&list); + free_reflist_inner(&list); } void cgit_print_refs(void) diff --git a/source/ui-repolist.c b/source/ui-repolist.c index 6df70e4..4e4f61b 100644 --- a/source/ui-repolist.c +++ b/source/ui-repolist.c @@ -27,11 +27,10 @@ struct sort_column { static time_t read_agefile(const char *path) { time_t result; - size_t size; char *buf = NULL; struct strbuf date = STRBUF_INIT; - if (cgit_read_first_line(path, &buf, &size)) { + if (cgit_read_first_line(path, &buf)) { free(buf); return 0; } @@ -77,7 +76,7 @@ static const char *tip_branch(const struct cgit_repo *repo, struct strbuf *head) return branch ? branch : "master"; } -static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime) +static time_t get_repo_modtime(const struct cgit_repo *repo) { struct strbuf path = STRBUF_INIT; struct strbuf head = STRBUF_INIT; @@ -87,39 +86,30 @@ static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime) // is kept in the repository itself so one page stats it only once. struct cgit_repo *writable = (struct cgit_repo *)repo; - if (repo->mtime != -1) { - *mtime = repo->mtime; - return 1; - } + if (repo->mtime != -1) + return repo->mtime; // A repository configured without a path has nothing on disk to date. if (!repo->path) { - *mtime = 0; writable->mtime = 0; return 0; } strbuf_addf(&path, "%s/%s", repo->path, ctx.cfg.agefile); - if (stat(path.buf, &st) == 0) { - *mtime = read_agefile(path.buf); - if (*mtime) { - writable->mtime = *mtime; - goto end; - } - } + writable->mtime = read_agefile(path.buf); + if (repo->mtime) + goto end; branch = tip_branch(repo, &head); strbuf_reset(&path); strbuf_addf(&path, "%s/refs/heads/%s", repo->path, branch); if (stat(path.buf, &st) == 0) { - *mtime = st.st_mtime; - writable->mtime = *mtime; + writable->mtime = st.st_mtime; goto end; } strbuf_reset(&path); strbuf_addf(&path, "%s/packed-refs", repo->path); if (stat(path.buf, &st) == 0) { - *mtime = st.st_mtime; - writable->mtime = *mtime; + writable->mtime = st.st_mtime; goto end; } @@ -127,26 +117,12 @@ static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime) // every ref update. strbuf_reset(&path); strbuf_addf(&path, "%s/reftable/tables.list", repo->path); - if (stat(path.buf, &st) == 0) { - *mtime = st.st_mtime; - writable->mtime = *mtime; - goto end; - } - - *mtime = 0; - writable->mtime = *mtime; + if (stat(path.buf, &st) == 0) + writable->mtime = st.st_mtime; end: strbuf_release(&path); strbuf_release(&head); - return (writable->mtime != 0); -} - -static void print_modtime(struct cgit_repo *repo) -{ - time_t mtime; - - if (get_repo_modtime(repo, &mtime)) - cgit_print_age(mtime, 0, -1); + return repo->mtime; } static int matches_search(struct cgit_repo *repo) @@ -247,11 +223,13 @@ static void print_repo_row(const char *currenturl, int sublevel) cgit_summary_link(ctx.repo->name, NULL, NULL, NULL); html("</td><td class='col-desc'>"); repourl = cgit_repourl(ctx.repo->url); - html_link_open(repourl, NULL, NULL); + html("<a href='"); + html_attr(repourl); + html("'>"); free(repourl); if (html_ntxt(ctx.repo->desc, ctx.cfg.max_repodesc_len) < 0) html("..."); - html_link_close(); + html("</a>"); html("</td>"); if (ctx.cfg.enable_index_owner) { html("<td class='col-owner'>"); @@ -269,7 +247,7 @@ static void print_repo_row(const char *currenturl, int sublevel) html("</td>"); } html("<td class='col-idle'>"); - print_modtime(ctx.repo); + cgit_print_age(get_repo_modtime(ctx.repo), 0, -1); html("</td>"); if (ctx.cfg.enable_index_links) { html("<td class='col-links'>"); @@ -340,11 +318,8 @@ static int cmp_idle(const void *a, const void *b) { const struct cgit_repo *r1 = a; const struct cgit_repo *r2 = b; - time_t t1, t2; + time_t t1 = get_repo_modtime(r1), t2 = get_repo_modtime(r2); - t1 = t2 = 0; - get_repo_modtime(r1, &t1); - get_repo_modtime(r2, &t2); // Only the sign is returned, because a 64-bit difference truncated // into an int could come back with the wrong sign and leave the // ordering inconsistent. @@ -377,11 +352,10 @@ static int cmp_section(const void *a, const void *b) */ static void resolve_modtimes(void) { - time_t t; int i; for (i = 0; i < cgit_repolist.count; i++) - get_repo_modtime(&cgit_repolist.repos[i], &t); + get_repo_modtime(&cgit_repolist.repos[i]); } static const struct sort_column sort_columns[] = { @@ -402,7 +376,7 @@ static int sort_repolist(const char *field) continue; if (column->cmp == cmp_idle || column->cmp == cmp_section) resolve_modtimes(); - cgit_qsort(cgit_repolist.repos, cgit_repolist.count, sizeof(struct cgit_repo), column->cmp); + QSORT(cgit_repolist.repos, cgit_repolist.count, column->cmp); return 1; } return 0; @@ -429,9 +403,7 @@ void cgit_print_repolist(void) ++columns; ctx.page.title = ctx.cfg.root_title; - cgit_print_http_headers(); - cgit_print_docstart(); - cgit_print_pageheader(); + cgit_print_layout_start(); if (ctx.qry.sort) column_sorted = sort_repolist(ctx.qry.sort); @@ -473,7 +445,7 @@ void cgit_print_repolist(void) if (hits > ctx.cfg.max_repo_count) print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search, ctx.qry.sort); free(currenturl); - cgit_print_docend(); + cgit_print_layout_end(); } void cgit_print_site_readme(void) diff --git a/source/ui-shared.c b/source/ui-shared.c index 936a6a4..81db404 100644 --- a/source/ui-shared.c +++ b/source/ui-shared.c This diff is too large to be rendered inline. View it on its own page. diff --git a/source/ui-shared.h b/source/ui-shared.h index da1b889..cd937a3 100644 --- a/source/ui-shared.h +++ b/source/ui-shared.h @@ -3,15 +3,14 @@ * handler renders its own body and calls in here for all that surrounds it, * which is the HTTP headers, the document head and footer, the header block * with its tabs and search form, the error page, and an anchor to any other - * cgit page. Whether a site addresses a request as a path below a virtual root - * or as a query string is settled behind these calls, so that nothing else has - * to know which of the two it is looking at. + * cgit page. */ #ifndef CGIT_UI_SHARED_H #define CGIT_UI_SHARED_H #include "cgit.h" + extern const char *cgit_httpscheme(void); extern char *cgit_hosturl(void); @@ -33,7 +32,6 @@ extern const char *cgit_loginurl(void); * result still needs escaping for wherever it lands. */ extern char *cgit_repourl(const char *reponame); -extern void cgit_print_clone_row(const char *url, int colspan); extern char *cgit_fileurl(const char *reponame, const char *pagename, const char *filename, const char *query); extern char *cgit_pageurl(const char *reponame, const char *pagename, const char *query); @@ -52,6 +50,14 @@ extern void cgit_tree_url(const char *head, struct strbuf *base, struct strbuf * extern void cgit_add_clone_urls(void (*fn)(const char *)); /* + * One row of the clone table. Every row is a link, but a browser cannot + * follow the ssh, scp or git forms, so those rows point at the first http + * url in the clone list, or at the repository's own page when the list has + * none. A colspan of zero leaves the attribute out. + */ +extern void cgit_print_clone_row(const char *url, int colspan); + +/* * Each of these writes one anchor to the page and nothing around it, with name * as the text a reader sees. A NULL title or class leaves that attribute out, * and what follows them is the state the target page should open in. @@ -101,6 +107,26 @@ extern struct date_mode cgit_date_mode(enum date_mode_type type); * relative dates off gets the date either way. */ extern void cgit_print_age(time_t t, int tz, time_t max_relative); + +/* + * Print date as a <time> element, readable on the page and exact in its + * datetime attribute. + */ +extern void cgit_print_date(timestamp_t date, int tz); + +/* + * Print the anchor of every line from first to last, each linking to itself. + * The anchors are handed over in batches rather than a write per line, and + * never built whole, which would come to several times the blob's size. + */ +extern void cgit_print_line_anchors(size_t first, size_t last); + +/* + * Print name through the email filter, followed by the address when + * show_email is set and enable-plain-email is on. A missing address reaches + * the filter as an empty string, since a filter must not be handed NULL. + */ +extern void cgit_print_person(const char *name, const char *email, const char *origin, int show_email); extern void cgit_print_http_headers(void); /* * Answer with a redirect to url, followed by query when one is given. Both @@ -108,24 +134,49 @@ extern void cgit_print_http_headers(void); * builds it, and only a byte a header line cannot carry is encoded here. */ extern void cgit_redirect(const char *url, const char *query, bool permanent); -extern void cgit_print_docstart(void); -extern void cgit_print_docend(void); __attribute__((format (printf,3,4))) extern void cgit_print_error_page(int code, const char *msg, const char *fmt, ...); -extern void cgit_vprint_error_page(int code, const char *msg, const char *fmt, va_list ap); /* * Answer with the error page for a blob of size bytes past max-blob-size. */ extern void cgit_print_blob_too_large(size_t size); -extern void cgit_print_pageheader(void); + +/* + * Resolve rev to the parsed commit it names, peeling a tag, with oid set to + * what rev itself names. Returns NULL once the 404 page naming rev is out. + */ +extern struct commit *cgit_get_commit(const char *rev, struct object_id *oid); + +/* + * Fill oid with the older end of a comparison with commit, which is what + * old_rev names when it is given, else commit's first parent, else the null + * id for a root commit. Returns -1 once the 404 page for old_rev is out. + */ +extern int cgit_get_old_oid(const char *old_rev, const struct commit *commit, struct object_id *oid); + +/* + * Start rev's walk, or print the error page and return non-zero when git + * cannot. A failed setup leaves the walk holding freed commits, so the caller + * must then neither read from it nor hand it to release_revisions. + */ +extern int cgit_prepare_walk(struct rev_info *rev); + +/* + * Refuse a path with a leading colon, printing the 400 page. git reads such a + * path as a pathspec with magic, which can end the request inside git. + * Returns non-zero when the path was refused. + */ +extern int cgit_refuse_pathspec_magic(const char *path); + extern void cgit_print_filemode(unsigned short mode); extern void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref, const char *separator); /* * The name a snapshot of this repository is downloaded under, which is the - * configured prefix or the last component of the repository URL. + * configured prefix or the last component of the repository URL. A name taken + * from the URL is only good until the next call. */ extern const char *cgit_snapshot_prefix(const struct cgit_repo *repo); @@ -135,11 +186,17 @@ extern const char *cgit_snapshot_prefix(const struct cgit_repo *repo); * terms are carried only when the caller asks for them, since a form with a * field of its own for either would otherwise submit that name twice. */ -extern void cgit_add_hidden_formfields(int incl_head, int incl_search, const char *page); +extern void cgit_add_hidden_formfields(int incl_head, int incl_search); /* * Put path in front of the page title, its last component first. */ extern void cgit_set_title_from_path(const char *path); + +/* + * Names the page ahead of the repository and the site, so that the pages of one + * repository can be told apart in tabs, history and search results. + */ extern void cgit_prefix_title(const char *name); + #endif // CGIT_UI_SHARED_H diff --git a/source/ui-snapshot.c b/source/ui-snapshot.c index bd5ebf2..333bce6 100644 --- a/source/ui-snapshot.c +++ b/source/ui-snapshot.c @@ -1,13 +1,13 @@ /* * Serves a commit as a downloadable archive. Writing the archive is git's - * work, so each format here is a thin wrapper that either calls write_archive - * directly or pipes its tar output through an external compressor. The table - * of formats also lives here, and since a repository's snapshots mask is one - * bit per table position, that order is part of what cgitrc means. When the - * request carries no revision the file name is worked backwards to find one, - * so that a link to cgit-1.2.tar.gz can serve the tag it was named after. A - * name ending in .asc asks for the detached signature kept under refs/notes - * instead of the archive itself. + * work, so each format here is only a row in a table, naming the archive + * format git writes and, for a compressed tar, the compressor its output is + * piped through. A repository's snapshots mask is one bit per table position, + * so that order is part of what cgitrc means. When the request carries no + * revision the file name is worked backwards to find one, so that a link to + * cgit-1.2.tar.gz can serve the tag it was named after. A name ending in .asc + * asks for the detached signature kept under refs/notes instead of the archive + * itself. */ #define USE_THE_REPOSITORY_VARIABLE @@ -19,13 +19,10 @@ #include "ui-shared.h" #include "ui-snapshot.h" -#define SIG_SUFFIX ".asc" - -static int write_archive_format(const char *format_arg, const char *hex, const char *prefix) +static void write_archive_format(const char *format_arg, const char *hex, const char *prefix) { struct strvec argv = STRVEC_INIT; const char **args; - int result; strvec_push(&argv, "snapshot"); strvec_push(&argv, format_arg); @@ -48,20 +45,25 @@ static int write_archive_format(const char *format_arg, const char *hex, const c args = xmalloc(sizeof(char *) * (argv.nr + 1)); memcpy(args, argv.v, sizeof(char *) * (argv.nr + 1)); - result = write_archive(argv.nr, args, NULL, the_repository, NULL, 0); + write_archive(argv.nr, args, NULL, the_repository, NULL, 0); strvec_clear(&argv); free(args); - return result; } -static int write_tar_archive(const char *hex, const char *prefix) +static void write_snapshot(const struct cgit_snapshot_format *format, + const char *hex, const char *prefix) { - return write_archive_format("--format=tar", hex, prefix); -} + struct cgit_exec_filter compressor; + struct cgit_filter *filter = NULL; -static int write_zip_archive(const char *hex, const char *prefix) -{ - return write_archive_format("--format=zip", hex, prefix); + if (format->compressor) { + cgit_exec_filter_init(&compressor, format->compressor[0], + format->compressor); + filter = &compressor.base; + } + cgit_open_filter(filter); + write_archive_format(format->format_arg, hex, prefix); + cgit_close_filter(filter); } static int program_on_path(const char *program) @@ -101,66 +103,25 @@ static int program_on_path(const char *program) return found; } -static int write_compressed_tar_archive(const char *hex, const char *prefix, const char *argv[]) -{ - struct cgit_exec_filter filter; - int result; - - cgit_exec_filter_init(&filter, argv[0], argv); - cgit_open_filter(&filter.base); - result = write_tar_archive(hex, prefix); - cgit_close_filter(&filter.base); - return result; -} - -static int write_tar_gzip_archive(const char *hex, const char *prefix) -{ - const char *argv[] = { "gzip", "-n", NULL }; - - return write_compressed_tar_archive(hex, prefix, argv); -} - -static int write_tar_bzip2_archive(const char *hex, const char *prefix) -{ - const char *argv[] = { "bzip2", NULL }; - - return write_compressed_tar_archive(hex, prefix, argv); -} - -static int write_tar_lzip_archive(const char *hex, const char *prefix) -{ - const char *argv[] = { "lzip", NULL }; - - return write_compressed_tar_archive(hex, prefix, argv); -} - -static int write_tar_xz_archive(const char *hex, const char *prefix) -{ - // Current xz runs a thread per core unless told otherwise. - const char *argv[] = { "xz", "-T1", NULL }; - - return write_compressed_tar_archive(hex, prefix, argv); -} - -static int write_tar_zstd_archive(const char *hex, const char *prefix) -{ - // Single-threaded like the other compressors, since -T0 would let one - // request pin every core. - const char *argv[] = { "zstd", NULL }; - - return write_compressed_tar_archive(hex, prefix, argv); -} +static const char *gzip_argv[] = { "gzip", "-n", NULL }; +static const char *bzip2_argv[] = { "bzip2", NULL }; +static const char *lzip_argv[] = { "lzip", NULL }; +// Current xz runs a thread per core unless told otherwise. +static const char *xz_argv[] = { "xz", "-T1", NULL }; +// Single-threaded like the other compressors, since -T0 would let one +// request pin every core. +static const char *zstd_argv[] = { "zstd", NULL }; // ui-shared.c reads entry zero as the tar whose signature stands in for every // tar variant, so this order cannot change. const struct cgit_snapshot_format cgit_snapshot_formats[] = { - { ".tar", "application/x-tar", write_tar_archive, NULL }, - { ".tar.gz", "application/gzip", write_tar_gzip_archive, "gzip" }, - { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive, "bzip2" }, - { ".tar.lz", "application/x-lzip", write_tar_lzip_archive, "lzip" }, - { ".tar.xz", "application/x-xz", write_tar_xz_archive, "xz" }, - { ".tar.zst", "application/zstd", write_tar_zstd_archive, "zstd" }, - { ".zip", "application/zip", write_zip_archive, NULL }, + { ".tar", "application/x-tar", "--format=tar", NULL }, + { ".tar.gz", "application/gzip", "--format=tar", gzip_argv }, + { ".tar.bz2", "application/x-bzip2", "--format=tar", bzip2_argv }, + { ".tar.lz", "application/x-lzip", "--format=tar", lzip_argv }, + { ".tar.xz", "application/x-xz", "--format=tar", xz_argv }, + { ".tar.zst", "application/zstd", "--format=tar", zstd_argv }, + { ".zip", "application/zip", "--format=zip", NULL }, { 0 } }; @@ -248,50 +209,41 @@ static int candidate_resolves(const struct cgit_repo *repo, const char *stem, * any is tried as a branch, which a branch spelled like a tag's stem would * otherwise win. */ -static const char *ref_from_filename(const struct cgit_repo *repo, const char *filename, - const struct cgit_snapshot_format *format) +static char *ref_from_filename(const struct cgit_repo *repo, const char *stem) { struct strbuf rev = STRBUF_INIT; - char *stem = xstrndup(filename, strlen(filename) - strlen(format->suffix)); int found = candidate_resolves(repo, stem, &rev, 1) || candidate_resolves(repo, stem, &rev, 0); - free(stem); if (!found) strbuf_release(&rev); return found ? strbuf_detach(&rev, NULL) : NULL; } -static int send_snapshot(const struct cgit_snapshot_format *format, const char *hex, +static void send_snapshot(const struct cgit_snapshot_format *format, const char *hex, const char *prefix, const char *filename) { struct object_id oid; - if (repo_get_oid(the_repository, hex, &oid)) { - cgit_print_error_page(404, "Not Found", "Bad object id: %s", hex); - return 1; - } - if (!lookup_commit_reference(the_repository, &oid)) { - cgit_print_error_page(404, "Not Found", "Not a commit: %s", hex); - return 1; - } + if (!cgit_get_commit(hex, &oid)) + return; // A compressor that cannot run only shows up as a broken pipe once // the tar is under way, so it is looked for while a status can still // say so. - if (format->program && !program_on_path(format->program)) { - cgit_print_error_page(500, "Internal Server Error", "Unable to run %s", format->program); - return 1; + if (format->compressor && !program_on_path(format->compressor[0])) { + cgit_print_error_page(500, "Internal Server Error", "Unable to run %s", + format->compressor[0]); + return; } - ctx.page.mimetype = xstrdup(format->mimetype); - ctx.page.filename = xstrdup(filename); + ctx.page.mimetype = format->mimetype; + ctx.page.filename = filename; cgit_print_http_headers(); init_archivers(); // The archiver reads its arguments as a command line, so it gets the // resolved id and never the name. - format->write_func(oid_to_hex(&oid), prefix); - return 0; + write_snapshot(format, oid_to_hex(&oid), prefix); } -static int send_sig(const struct cgit_snapshot_format *format, const char *hex, +static void send_sig(const struct cgit_snapshot_format *format, const char *hex, const char *archive_name, const char *sig_filename) { const struct object_id *note = cgit_snapshot_get_sig(hex, format); @@ -301,34 +253,33 @@ static int send_sig(const struct cgit_snapshot_format *format, const char *hex, if (!note) { cgit_print_error_page(404, "Not Found", "No signature for %s", archive_name); - return 0; + return; } // A note is any blob a pusher stored, so it is held to the same bound // as every other blob a page inflates. if (odb_read_object_info(the_repository->objects, note, &size) != OBJ_BLOB) { cgit_print_error_page(404, "Not Found", "Not found"); - return 0; + return; } if (cgit_blob_too_large(size)) { cgit_print_blob_too_large(size); - return 0; + return; } buf = odb_read_object(the_repository->objects, note, &type, &size); if (!buf) { cgit_print_error_page(404, "Not Found", "Not found"); - return 0; + return; } ctx.page.untrusted = 1; - ctx.page.mimetype = xstrdup("application/pgp-signature"); - ctx.page.filename = xstrdup(sig_filename); + ctx.page.mimetype = "application/pgp-signature"; + ctx.page.filename = sig_filename; cgit_print_http_headers(); html_raw(buf, size); free(buf); - return 0; } const struct object_id *cgit_snapshot_get_sig(const char *ref, const struct cgit_snapshot_format *f) @@ -401,58 +352,51 @@ void cgit_print_snapshot(const char *head, const char *hex, const char *filename const char *sig_filename = NULL; char *archive_name = NULL; char *prefix = NULL; + char *ref = NULL; + size_t len; if (!filename) { cgit_print_error_page(400, "Bad Request", "No snapshot name specified"); return; } - if (ends_with(filename, SIG_SUFFIX)) { + if (strip_suffix(filename, SIG_SUFFIX, &len)) { sig_filename = filename; - archive_name = xstrdup(filename); - archive_name[strlen(archive_name) - strlen(SIG_SUFFIX)] = '\0'; - filename = archive_name; + filename = archive_name = xstrndup(filename, len); } f = find_format(filename); if (!f || (!sig_filename && !(ctx.repo->snapshots & cgit_snapshot_format_bit(f)))) { cgit_print_error_page(400, "Bad Request", "Unsupported snapshot format: %s", filename); - return; + goto done; } - // The name becomes the prefix of every member of the archive, and a - // revision expression such as :/pattern could carry a path in it. - if (!hex && dwim && strchr(filename, '/')) { - cgit_print_error_page(404, "Not Found", "Not found"); - return; - } - // A name that is all suffix names no revision, though the lookup below - // would settle on a ref called v. - if (!hex && dwim && strlen(filename) == strlen(f->suffix)) { - cgit_print_error_page(404, "Not Found", "Not found"); - return; - } if (!hex && dwim) { - hex = ref_from_filename(ctx.repo, filename, f); + strip_suffix(filename, f->suffix, &len); + prefix = xstrndup(filename, len); + // The stem becomes the prefix of every member of the archive, + // and a revision expression such as :/pattern could carry a path + // in it. An empty stem names no revision, though the lookup + // would settle on a ref called v. + if (len && !strchr(prefix, '/')) + hex = ref = ref_from_filename(ctx.repo, prefix); if (!hex) { cgit_print_error_page(404, "Not Found", "Not found"); - return; + goto done; } - prefix = xstrdup(filename); - prefix[strlen(filename) - strlen(f->suffix)] = '\0'; - } - - if (!hex) - hex = head; - - if (!prefix) + } else { prefix = xstrdup(cgit_snapshot_prefix(ctx.repo)); + if (!hex) + hex = head; + } if (sig_filename) send_sig(f, hex, filename, sig_filename); else send_snapshot(f, hex, prefix, filename); +done: free(prefix); + free(ref); free(archive_name); } diff --git a/source/ui-snapshot.h b/source/ui-snapshot.h index 412807e..12241a9 100644 --- a/source/ui-snapshot.h +++ b/source/ui-snapshot.h @@ -11,14 +11,13 @@ #include "cgit.h" -typedef int (*write_archive_fn_t)(const char *hex, const char *prefix); - struct cgit_snapshot_format { const char *suffix; const char *mimetype; - write_archive_fn_t write_func; - // The compressor the writer runs, or NULL when it needs none. - const char *program; + const char *format_arg; + // The command line git's output is piped through, or NULL when the + // archive goes out as git writes it. + const char **compressor; }; // Terminated by an entry with a NULL suffix. @@ -28,12 +27,15 @@ extern unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f); /* * Turn a cgitrc snapshots value into a mask over cgit_snapshot_formats. A - * plain nonzero number is the legacy boolean form and enables plain tar + * plain non-zero number is the legacy boolean form and enables plain tar * alone, the word all enables every format, and otherwise the value is a * space separated list of suffixes. */ extern int cgit_parse_snapshots_mask(const char *str); +// The suffix that names a signature after the archive it signs. +#define SIG_SUFFIX ".asc" + /* * The detached signature stored for this format under refs/notes, if any. */ diff --git a/source/ui-ssdiff.c b/source/ui-ssdiff.c index afa25ea..90498b7 100644 --- a/source/ui-ssdiff.c +++ b/source/ui-ssdiff.c @@ -14,6 +14,14 @@ #include "ui-shared.h" #include "ui-ssdiff.h" +// The longest old line and the longest new line the view will compare +// character by character. The comparison fills a cell for every pair of +// characters, so its cost is the product of the two, and a line past the limit +// is shown whole instead. +#define MAX_SSDIFF_M 128 +#define MAX_SSDIFF_N 128 +#define MAX_SSDIFF_SIZE (MAX_SSDIFF_M * MAX_SSDIFF_N) + // One line held back until its run ends, owning the copy taken of it. struct deferred_line { int line_no; @@ -21,6 +29,8 @@ struct deferred_line { struct deferred_line *next; }; +// The pair being rendered, whose two sides the line number links lead into. +static struct diff_filepair *current_filepair; static int current_old_line, current_new_line; static int **lcs_table; static struct deferred_line *deferred_old, *deferred_old_last; @@ -182,6 +192,27 @@ static void print_lineno_cell(struct diff_filespec *file, const struct object_id strbuf_release(&path); } +/* + * mark is the class for the characters this side of the row does not share + * with the other. + */ +static void print_side(const char *class, struct diff_filespec *file, const struct object_id *rev, + int line_no, const char *line, const char *lcs, const char *mark) +{ + if (line_no > 0) + print_lineno_cell(file, rev, line_no); + else + html("<td class='lineno'></td>"); + htmlf("<td class='%s%s'>", class, line ? "" : "-dark"); + if (line) { + if (lcs) + print_line_with_lcs(mark, line, lcs); + else + html_txt(line); + } + html("</td>"); +} + static void print_row(const char *class, int old_line_no, char *old_line, int new_line_no, char *new_line, int highlight_chars) { @@ -195,38 +226,10 @@ static void print_row(const char *class, int old_line_no, char *old_line, if (highlight_chars && old_line && new_line) lcs = longest_common_subsequence(old_line, new_line); html("<tr>\n"); - if (old_line_no > 0) { - print_lineno_cell(cgit_get_current_old_file(), old_rev_oid, old_line_no); - htmlf("<td class='%s'>", class); - } else if (old_line) { - htmlf("<td class='lineno'></td><td class='%s'>", class); - } else { - htmlf("<td class='lineno'></td><td class='%s-dark'>", class); - } - if (old_line) { - if (lcs) - print_line_with_lcs("del", old_line, lcs); - else - html_txt(old_line); - } - - html("</td>\n"); - if (new_line_no > 0) { - print_lineno_cell(cgit_get_current_new_file(), new_rev_oid, new_line_no); - htmlf("<td class='%s'>", class); - } else if (new_line) { - htmlf("<td class='lineno'></td><td class='%s'>", class); - } else { - htmlf("<td class='lineno'></td><td class='%s-dark'>", class); - } - if (new_line) { - if (lcs) - print_line_with_lcs("add", new_line, lcs); - else - html_txt(new_line); - } - - html("</td></tr>\n"); + print_side(class, current_filepair->one, old_rev_oid, old_line_no, old_line, lcs, "del"); + html("\n"); + print_side(class, current_filepair->two, new_rev_oid, new_line_no, new_line, lcs, "add"); + html("</tr>\n"); free(lcs); free(new_line); free(old_line); @@ -268,38 +271,12 @@ static int count_deferred(struct deferred_line *item) return count; } -static void print_deferred_old_lines(void) -{ - struct deferred_line *item = deferred_old; - struct deferred_line *next; - - while (item) { - print_row("del", item->line_no, item->line, -1, NULL, 0); - next = item->next; - free_deferred(item); - item = next; - } -} - -static void print_deferred_new_lines(void) -{ - struct deferred_line *item = deferred_new; - struct deferred_line *next; - - while (item) { - print_row("add", -1, NULL, item->line_no, item->line, 0); - next = item->next; - free_deferred(item); - item = next; - } -} - /* * Pairing a removal with an addition only stands for anything when the two * runs are the same length, which is why the character marking is offered only * then. */ -static void print_deferred_changed_lines(void) +static void print_deferred_run(const char *class) { struct deferred_line *old_item = deferred_old; struct deferred_line *new_item = deferred_new; @@ -309,12 +286,12 @@ static void print_deferred_changed_lines(void) highlight_chars = count_deferred(old_item) == count_deferred(new_item); while (old_item || new_item) { if (old_item && new_item) - print_row("changed", old_item->line_no, old_item->line, + print_row(class, old_item->line_no, old_item->line, new_item->line_no, new_item->line, highlight_chars); else if (old_item) - print_row("changed", old_item->line_no, old_item->line, -1, NULL, 0); + print_row(class, old_item->line_no, old_item->line, -1, NULL, 0); else if (new_item) - print_row("changed", -1, NULL, new_item->line_no, new_item->line, 0); + print_row(class, -1, NULL, new_item->line_no, new_item->line, 0); if (old_item) { next = old_item->next; free_deferred(old_item); @@ -353,12 +330,7 @@ static void print_deferred_lines(void) { if (!deferred_old && !deferred_new) return; - if (deferred_old && !deferred_new) - print_deferred_old_lines(); - else if (!deferred_old && deferred_new) - print_deferred_new_lines(); - else - print_deferred_changed_lines(); + print_deferred_run(!deferred_new ? "del" : !deferred_old ? "add" : "changed"); deferred_old = deferred_old_last = NULL; deferred_new = deferred_new_last = NULL; } @@ -401,12 +373,13 @@ void cgit_ssdiff_line_cb(char *line, int len) line[len - 1] = terminator; } -void cgit_ssdiff_header_begin(const char *path) +void cgit_ssdiff_header_begin(struct diff_filepair *pair) { + current_filepair = pair; current_old_line = -1; current_new_line = -1; html("<tbody class='file' data-path='"); - html_attr(path); + html_attr(pair->two->path); html("'>\n"); html("<tr><td class='space' colspan='4'></td></tr>\n"); html("<tr><td class='head' colspan='4'>"); diff --git a/source/ui-ssdiff.h b/source/ui-ssdiff.h index 9417277..e40e9b8 100644 --- a/source/ui-ssdiff.h +++ b/source/ui-ssdiff.h @@ -7,26 +7,15 @@ #ifndef CGIT_UI_SSDIFF_H #define CGIT_UI_SSDIFF_H -// The longest old line and the longest new line the view will compare -// character by character. The comparison fills a cell for every pair of -// characters, so its cost is the product of the two, and a line past the limit -// is shown whole instead. -#ifndef MAX_SSDIFF_M -#define MAX_SSDIFF_M 128 -#endif - -#ifndef MAX_SSDIFF_N -#define MAX_SSDIFF_N 128 -#endif -#define MAX_SSDIFF_SIZE ((MAX_SSDIFF_M) * (MAX_SSDIFF_N)) - extern void cgit_ssdiff_line_cb(char *line, int len); /* - * Open the group of rows one file's diff occupies, header row included, and - * close it again after the last line. path lands on the group as data-path. + * Open the group of rows a file pair's diff occupies, header row included, and + * close it again after the last line. The new path lands on the group as + * data-path, and the rows read the pair until the footer, so it has to live + * that long. */ -extern void cgit_ssdiff_header_begin(const char *path); +extern void cgit_ssdiff_header_begin(struct diff_filepair *pair); extern void cgit_ssdiff_header_end(void); extern void cgit_ssdiff_footer(void); diff --git a/source/ui-stats.c b/source/ui-stats.c index e4dcf8e..ee5b76b 100644 --- a/source/ui-stats.c +++ b/source/ui-stats.c @@ -1,9 +1,7 @@ /* - * The statistics page, which counts commits per author across a window of - * recent weeks, months, quarters or years. The window sizes live in one table - * here, which cgit.c also resolves the max-stats setting against so a - * repository can refuse the coarser windows. The page reads no diff and no - * blob contents, so the whole thing costs about one rev-list. + * The statistics page counts each author's commits in the most recent periods + * of the size asked for. It reads no diff and no blob contents, so the whole + * thing costs about one rev-list. */ #define USE_THE_REPOSITORY_VARIABLE @@ -11,12 +9,29 @@ #include "cgit.h" #include "html.h" #include "parsing.h" -#include "shared.h" #include "ui-shared.h" #include "ui-stats.h" #define DEFAULT_AUTHOR_ROWS 10 +// How many periods the page shows side by side. +#define PERIOD_COLUMNS 4 + +struct cgit_period { + const char code; + const char *name; + + // Move a tm back to the first day of the period it falls in. + void (*trunc)(struct tm *tm); + + // Step a truncated tm n whole periods, back when n is negative. + void (*step)(struct tm *tm, int n); + + // Label the period a tm falls in. The buffer is reused, so a caller + // that keeps a label copies it. + char *(*pretty)(struct tm *tm); +}; + // One author's share of the window. periods is keyed by the label of a period, // with the commit count stored in the util field itself rather than behind // another allocation. @@ -35,19 +50,11 @@ static void trunc_week(struct tm *tm) gmtime_r(&t, tm); } -static void dec_week(struct tm *tm) -{ - time_t t = timegm(tm); - - t -= SECONDS_PER_WEEK; - gmtime_r(&t, tm); -} - -static void inc_week(struct tm *tm) +static void step_week(struct tm *tm, int n) { time_t t = timegm(tm); - t += SECONDS_PER_WEEK; + t += n * SECONDS_PER_WEEK; gmtime_r(&t, tm); } @@ -68,21 +75,15 @@ static void trunc_month(struct tm *tm) tm->tm_mday = 1; } -static void dec_month(struct tm *tm) +static void step_month(struct tm *tm, int n) { - tm->tm_mon--; + tm->tm_mon += n; if (tm->tm_mon < 0) { tm->tm_year--; - tm->tm_mon = 11; - } -} - -static void inc_month(struct tm *tm) -{ - tm->tm_mon++; - if (tm->tm_mon > 11) { + tm->tm_mon += 12; + } else if (tm->tm_mon > 11) { tm->tm_year++; - tm->tm_mon = 0; + tm->tm_mon -= 12; } } @@ -98,22 +99,12 @@ static char *pretty_month(struct tm *tm) static void trunc_quarter(struct tm *tm) { trunc_month(tm); - while (tm->tm_mon % 3 != 0) - dec_month(tm); + tm->tm_mon -= tm->tm_mon % 3; } -static void dec_quarter(struct tm *tm) +static void step_quarter(struct tm *tm, int n) { - dec_month(tm); - dec_month(tm); - dec_month(tm); -} - -static void inc_quarter(struct tm *tm) -{ - inc_month(tm); - inc_month(tm); - inc_month(tm); + step_month(tm, 3 * n); } static char *pretty_quarter(struct tm *tm) @@ -127,14 +118,9 @@ static void trunc_year(struct tm *tm) tm->tm_mon = 0; } -static void dec_year(struct tm *tm) -{ - tm->tm_year--; -} - -static void inc_year(struct tm *tm) +static void step_year(struct tm *tm, int n) { - tm->tm_year++; + tm->tm_year += n; } static char *pretty_year(struct tm *tm) @@ -145,10 +131,10 @@ static char *pretty_year(struct tm *tm) // The order runs from the finest window to the coarsest, because a repository // caps the page by storing an index into this table as its max-stats. static const struct cgit_period periods[] = { - {'w', "week", 4, trunc_week, dec_week, inc_week, pretty_week}, - {'m', "month", 4, trunc_month, dec_month, inc_month, pretty_month}, - {'q', "quarter", 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter}, - {'y', "year", 4, trunc_year, dec_year, inc_year, pretty_year}, + {'w', "week", trunc_week, step_week, pretty_week}, + {'m', "month", trunc_month, step_month, pretty_month}, + {'q', "quarter", trunc_quarter, step_quarter, pretty_quarter}, + {'y', "year", trunc_year, step_year, pretty_year}, }; static void window_start(const struct cgit_period *period, struct tm *tm) @@ -162,8 +148,12 @@ static void window_start(const struct cgit_period *period, struct tm *tm) time(&now); gmtime_r(&now, tm); period->trunc(tm); - for (i = 1; i < period->count; i++) - period->dec(tm); + // The window opens at midnight, not at the time of day it is now. + tm->tm_hour = 0; + tm->tm_min = 0; + tm->tm_sec = 0; + for (i = 1; i < PERIOD_COLUMNS; i++) + period->step(tm, -1); } static void add_commit(struct string_list *authors, struct commitinfo *info, @@ -186,13 +176,12 @@ static void add_commit(struct string_list *authors, struct commitinfo *info, stats = author->util; when = info->committer_date; // A crafted commit can carry a date outside the range gmtime_r can - // represent, which would leave date uninitialized and later index the + // represent, which would leave date uninitialised and later index the // month table out of bounds. if (!gmtime_r(&when, &date)) return; period->trunc(&date); label = xstrdup(period->pretty(&date)); - // The pointer field holds the count itself. bucket = string_list_insert(&stats->periods, label); if (bucket->util) free(label); @@ -211,38 +200,25 @@ static int collect_stats(const struct cgit_period *period, struct string_list *a { struct rev_info rev; struct commit *commit; - // setup_revisions reads these the way main does, skipping the first - // entry, and reads past the count up to a NULL after the double dash, - // so the sentinel has to stay even when the path fills the slot before - // it. - const char *argv[] = { NULL, ctx.qry.head, NULL, NULL, NULL }; - int argc = 2; + struct strvec argv = STRVEC_INIT; time_t since; struct tm tm; window_start(period, &tm); since = timegm(&tm); - if (ctx.qry.path) { - argv[2] = "--"; - argv[3] = ctx.qry.path; - argc += 2; - } + strvec_pushl(&argv, "stats", ctx.qry.head, NULL); + if (ctx.qry.path) + strvec_pushl(&argv, "--", ctx.qry.path, NULL); repo_init_revisions(the_repository, &rev, NULL); - rev.abbrev = DEFAULT_ABBREV; - rev.commit_format = CMIT_FMT_DEFAULT; rev.max_parents = 1; - rev.verbose_header = 1; - rev.show_root_diff = 0; - setup_revisions(argc, argv, &rev, NULL); + setup_revisions_from_strvec(&argv, &rev, NULL); // Prune the walk to the displayed window instead of traversing the // whole history and discarding older commits. The check below still // bounds the period edge exactly. rev.max_age = since; - // A failed setup leaves the walk holding freed commits, so it must - // not be read from. - if (prepare_revision_walk(&rev)) { - cgit_print_error_page(500, "Internal Server Error", "Unable to read the history"); + if (cgit_prepare_walk(&rev)) { + strvec_clear(&argv); return -1; } while ((commit = get_revision(&rev))) { @@ -255,6 +231,8 @@ static int collect_stats(const struct cgit_period *period, struct string_list *a release_commit_memory(the_repository->parsed_objects, commit); commit->parents = NULL; } + release_revisions(&rev); + strvec_clear(&argv); return 0; } @@ -286,9 +264,9 @@ static struct string_list build_period_labels(const struct cgit_period *period) int i; window_start(period, &tm); - for (i = 0; i < period->count; i++) { + for (i = 0; i < PERIOD_COLUMNS; i++) { string_list_append(&labels, period->pretty(&tm)); - period->inc(&tm); + period->step(&tm, 1); } return labels; } @@ -297,9 +275,9 @@ static struct string_list build_period_labels(const struct cgit_period *period) * The run of authors to sum is given as a start and a count rather than as * two indices, so that an empty author list cannot describe a run that wraps. */ -static void print_summary_row(struct string_list *authors, size_t from, size_t count, - const char *label_format, const char *leftclass, const char *centerclass, - const char *rightclass, const struct string_list *labels) +static void print_row(struct string_list *authors, size_t from, size_t count, + const char *name, const char *nameclass, const char *cellclass, + const struct string_list *labels) { struct authorstat *stats; struct string_list_item *bucket; @@ -307,7 +285,9 @@ static void print_summary_row(struct string_list *authors, size_t from, size_t c long total, subtotal; total = 0; - htmlf("<tr><td class='%s'>%s</td>", leftclass, cgit_fmt(label_format, (long)count)); + htmlf("<tr><td class='%s'>", nameclass); + html_txt(name); + html("</td>"); for (column = 0; column < labels->nr; column++) { const char *label = labels->items[column].string; @@ -318,18 +298,18 @@ static void print_summary_row(struct string_list *authors, size_t from, size_t c if (bucket) subtotal += (uintptr_t)bucket->util; } - htmlf("<td class='%s'>%ld</td>", centerclass, subtotal); + if (cellclass) + htmlf("<td class='%s'>%ld</td>", cellclass, subtotal); + else + htmlf("<td>%ld</td>", subtotal); total += subtotal; } - htmlf("<td class='%s'>%ld</td></tr>\n", rightclass, total); + htmlf("<td class='sum'>%ld</td></tr>\n", total); } static void print_authors(struct string_list *authors, int max_rows, const struct string_list *labels) { - struct string_list_item *author, *bucket; - struct authorstat *stats; size_t i, column, rows; - long total; html("<table class='stats'>\n<tr><th scope='col'>Author</th>"); for (column = 0; column < labels->nr; column++) @@ -339,32 +319,14 @@ static void print_authors(struct string_list *authors, int max_rows, const struc // The row count arrives through ofs, which carries -1 for "all". rows = (max_rows <= 0 || (size_t)max_rows > authors->nr) ? authors->nr : (size_t)max_rows; - for (i = 0; i < rows; i++) { - author = &authors->items[i]; - html("<tr><td class='left'>"); - html_txt(author->string); - html("</td>"); - stats = author->util; - total = 0; - for (column = 0; column < labels->nr; column++) { - const char *label = labels->items[column].string; - - bucket = string_list_lookup(&stats->periods, label); - if (!bucket) - html("<td>0</td>"); - else { - htmlf("<td>%ju</td>", (uintmax_t)(uintptr_t)bucket->util); - total += (uintptr_t)bucket->util; - } - } - htmlf("<td class='sum'>%ld</td></tr>\n", total); - } + for (i = 0; i < rows; i++) + print_row(authors, i, 1, authors->items[i].string, "left", NULL, labels); if (rows < authors->nr) - print_summary_row(authors, rows, authors->nr - rows, "Others (%ld)", "left", "", "sum", - labels); + print_row(authors, rows, authors->nr - rows, + cgit_fmt("Others (%ld)", (long)(authors->nr - rows)), "left", NULL, labels); - print_summary_row(authors, 0, authors->nr, "Total", "total", "sum", "sum", labels); + print_row(authors, 0, authors->nr, "Total", "total", "sum", labels); html("</table>\n"); } @@ -375,7 +337,7 @@ static void print_options_form(const struct cgit_period *period, int top) html("<div class='cgit-panel'>\n"); html("<strong>Stat options</strong>"); html("<form method='get' aria-label='Stat options'>"); - cgit_add_hidden_formfields(1, 0, "stats"); + cgit_add_hidden_formfields(1, 0); html("<div class='fields'>"); if (ctx.repo->max_stats > 1) { choices = ctx.repo->max_stats; @@ -448,9 +410,11 @@ void cgit_show_stats(void) cgit_print_error_page(400, "Bad Request", "Statistics type disabled: %s", period->name); return; } + if (cgit_refuse_pathspec_magic(ctx.qry.path)) + return; if (collect_stats(period, &authors)) return; - cgit_qsort(authors.items, authors.nr, sizeof(struct string_list_item), cmp_total_commits); + QSORT(authors.items, authors.nr, cmp_total_commits); top = ctx.qry.ofs; if (!top) diff --git a/source/ui-stats.h b/source/ui-stats.h index 325e082..3479732 100644 --- a/source/ui-stats.h +++ b/source/ui-stats.h @@ -1,33 +1,13 @@ /* * The statistics page, which counts commits per author over a window of - * recent weeks, months, quarters or years. The windows are described here - * rather than inside the page, because cgit.c resolves a repository's - * max-stats setting against the same set. + * recent weeks, months, quarters or years, and the period lookups that cgit.c + * resolves a max-stats setting with. */ #ifndef CGIT_UI_STATS_H #define CGIT_UI_STATS_H -#include "cgit.h" - -struct cgit_period { - const char code; - const char *name; - - // How many periods a page shows side by side. - int count; - - // Move a tm back to the first day of the period it falls in. - void (*trunc)(struct tm *tm); - - // Step a truncated tm one whole period back or forward. - void (*dec)(struct tm *tm); - void (*inc)(struct tm *tm); - - // Label the period a tm falls in. The buffer is reused, so a caller - // that keeps a label copies it. - char *(*pretty)(struct tm *tm); -}; +struct cgit_period; /* * Look a period up by its one letter code or by its full name, storing the diff --git a/source/ui-summary.c b/source/ui-summary.c index f37650d..8cc48bf 100644 --- a/source/ui-summary.c +++ b/source/ui-summary.c @@ -10,30 +10,14 @@ #include "cgit.h" #include "filter.h" #include "html.h" -#include "shared.h" #include "ui-blob.h" #include "ui-log.h" #include "ui-refs.h" #include "ui-shared.h" #include "ui-summary.h" -// Age, commit message and author, the three columns a log row always has. -// ui-log.c adds one more for each of the two optional counts. -#define LOG_BASE_COLUMNS 3 - static int clone_urls_printed; -static int log_columns(void) -{ - int columns = LOG_BASE_COLUMNS; - - if (ctx.repo->enable_log_filecount) - columns++; - if (ctx.repo->enable_log_linecount) - columns++; - return columns; -} - /* * The sections share one table, which is as wide as the widest of them, and * every row fills it, since a short row would end its stripe short of the @@ -41,8 +25,8 @@ static int log_columns(void) */ static int table_columns(void) { - if (ctx.cfg.summary_log > 0 && log_columns() > REF_COLUMNS) - return log_columns(); + if (ctx.cfg.summary_log > 0 && cgit_log_columns(0) > REF_COLUMNS) + return cgit_log_columns(0); return REF_COLUMNS; } @@ -96,7 +80,7 @@ void cgit_print_repo_readme(void) // into safe HTML, so it is escaped rather than served raw. html("<pre class='plaintext'>"); if (ref) { - err = cgit_print_file(filename, ref, 1, 1); + err = cgit_print_file(filename, ref, 1); } else { struct strbuf sb = STRBUF_INIT; if (strbuf_read_file(&sb, filename, 0) >= 0) @@ -115,7 +99,7 @@ void cgit_print_repo_readme(void) // through unescaped. cgit_open_filter(ctx.repo->about_filter, filename, tree_base.buf, tree_query.buf); if (ref) - err = cgit_print_file(filename, ref, 1, 0); + err = cgit_print_file(filename, ref, 0); else html_include(filename); cgit_close_filter(ctx.repo->about_filter); diff --git a/source/ui-tag.c b/source/ui-tag.c index f9ce6b3..d60a58e 100644 --- a/source/ui-tag.c +++ b/source/ui-tag.c @@ -10,10 +10,8 @@ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" -#include "filter.h" #include "html.h" #include "parsing.h" -#include "shared.h" #include "ui-shared.h" #include "ui-tag.h" @@ -28,25 +26,15 @@ static void print_name_row(const char *revname, const struct object_id *oid) static void print_date_row(struct taginfo *info) { - html("<tr><th scope='row'>tag date</th><td><time datetime='"); - html_attr(show_date(info->tagger_date, info->tagger_tz, cgit_date_mode(DATE_ISO8601_STRICT))); - html("'>"); - html_txt(show_date(info->tagger_date, info->tagger_tz, cgit_date_mode(DATE_ISO8601))); - html("</time></td></tr>\n"); + html("<tr><th scope='row'>tag date</th><td>"); + cgit_print_date(info->tagger_date, info->tagger_tz); + html("</td></tr>\n"); } static void print_tagger_row(struct taginfo *info) { html("<tr><th scope='row'>tagged by</th><td>"); - // A tag can carry a tagger with no address, and a filter must not be - // handed a NULL argument. - cgit_open_filter(ctx.repo->email_filter, info->tagger_email ? info->tagger_email : "", "tag"); - html_txt(info->tagger); - if (info->tagger_email && ctx.cfg.enable_plain_email) { - html(" "); - html_txt(info->tagger_email); - } - cgit_close_filter(ctx.repo->email_filter); + cgit_print_person(info->tagger, info->tagger_email, "tag", 1); html("</td></tr>\n"); } @@ -98,20 +86,18 @@ static void print_message(char *msg) } } -static void print_annotated_tag(const char *revname, const struct object_id *oid) +static void print_annotated_tag(const char *revname, struct tag *tag) { - struct tag *tag; - struct taginfo *info; + struct taginfo *info = cgit_parse_tag(tag); - tag = lookup_tag(the_repository, oid); - if (!tag || parse_tag(the_repository, tag) || !(info = cgit_parse_tag(tag))) { + if (!info) { cgit_print_error_page(500, "Internal Server Error", "Bad tag object: %s", revname); return; } cgit_print_layout_start(); html("<table class='commit-info'>\n"); - print_name_row(revname, oid); + print_name_row(revname, &tag->object.oid); if (info->tagger_date > 0) print_date_row(info); if (info->tagger) @@ -167,7 +153,7 @@ void cgit_print_tag(char *revname) } cgit_prefix_title(revname); if (obj->type == OBJ_TAG) - print_annotated_tag(revname, &oid); + print_annotated_tag(revname, (struct tag *)obj); else print_lightweight_tag(revname, obj); diff --git a/source/ui-tree.c b/source/ui-tree.c index 95ff337..aff4f0f 100644 --- a/source/ui-tree.c +++ b/source/ui-tree.c @@ -38,7 +38,7 @@ struct ls_entry { }; struct walk_tree_context { - char *rev; + const char *rev; char *match_path; enum walk_state state; struct ls_entry *entries; @@ -54,38 +54,19 @@ struct only_child { size_t count; }; -static void add_linenumber(struct strbuf *numbers, size_t lineno) -{ - strbuf_addf(numbers, "<a id='n%zu' href='#n%zu'>%zu</a>\n", lineno, lineno, lineno); -} - -/* - * The anchors are handed over in batches rather than a write per line, and - * never built whole, which would come to several times the blob's size. - */ static void print_linenumbers(const char *buf, size_t size) { - struct strbuf numbers = STRBUF_INIT; - size_t lineno = 0, idx = 0; + size_t lines = 0, idx; if (size) { - add_linenumber(&numbers, ++lineno); - + lines = 1; // The newline that ends the last line must not open a line of - // its own, so the final byte is left out of the scan. - while (idx < size - 1) { - if (buf[idx] == '\n') { - add_linenumber(&numbers, ++lineno); - if (numbers.len >= HTML_BATCH) { - html_raw(numbers.buf, numbers.len); - strbuf_reset(&numbers); - } - } - idx++; - } - html_raw(numbers.buf, numbers.len); + // its own, so the final byte is left out of the count. + for (idx = 0; idx < size - 1; idx++) + if (buf[idx] == '\n') + lines++; } - strbuf_release(&numbers); + cgit_print_line_anchors(1, lines); } static void print_text_buffer(const char *filename, char *buf, size_t size) @@ -100,20 +81,14 @@ static void print_text_buffer(const char *filename, char *buf, size_t size) html("<tr>\n"); } + html("<td class='lines'><pre><code>"); if (ctx.repo->source_filter) { - char *filter_arg = xstrdup(filename); - - html("<td class='lines'><pre><code>"); - cgit_open_filter(ctx.repo->source_filter, filter_arg); + cgit_open_filter(ctx.repo->source_filter, filename); html_raw(buf, size); cgit_close_filter(ctx.repo->source_filter); - free(filter_arg); - html("</code></pre></td></tr></table>\n"); - return; + } else { + html_blob_txt(buf, size); } - - html("<td class='lines'><pre><code>"); - html_blob_txt(buf, size); html("</code></pre></td></tr></table>\n"); } @@ -231,8 +206,8 @@ static int only_child_cb(const struct object_id *oid, struct strbuf *base UNUSED return 0; } -static void print_dir_chain(const struct object_id *oid, char *name, char *rev, - struct strbuf *fullpath) +static void print_dir_chain(const struct object_id *oid, const char *name, + const char *rev, struct strbuf *fullpath) { size_t initial_length = fullpath->len; struct tree *tree; @@ -254,7 +229,7 @@ static void print_dir_chain(const struct object_id *oid, char *name, char *rev, tree = lookup_tree(the_repository, &child.oid); if (!tree) - return; + break; free(child.name); child.name = NULL; @@ -270,13 +245,13 @@ static void print_dir_chain(const struct object_id *oid, char *name, char *rev, name = child.name; } + free(child.name); strbuf_setlen(fullpath, initial_length); } -static void print_ls_row(const struct object_id *oid, const char *pathname, +static void print_ls_row(const struct object_id *oid, const char *name, unsigned mode, struct walk_tree_context *walk) { - char *name; struct strbuf fullpath = STRBUF_INIT; struct strbuf linkpath = STRBUF_INIT; struct strbuf class = STRBUF_INIT; @@ -284,7 +259,6 @@ static void print_ls_row(const struct object_id *oid, const char *pathname, size_t size = 0; char *buf; - name = xstrdup(pathname); strbuf_addf(&fullpath, "%s%s%s", ctx.qry.path ? ctx.qry.path : "", ctx.qry.path ? "/" : "", name); if (!S_ISGITLINK(mode)) { @@ -314,7 +288,7 @@ static void print_ls_row(const struct object_id *oid, const char *pathname, } else if (S_ISDIR(mode)) { print_dir_chain(oid, name, walk->rev, &fullpath); } else { - char *ext = strrchr(name, '.'); + const char *ext = strrchr(name, '.'); strbuf_addstr(&class, "ls-blob"); // The extension becomes a class a stylesheet can pick files out @@ -366,7 +340,6 @@ static void print_ls_row(const struct object_id *oid, const char *pathname, html("</td></tr>\n"); cleanup: - free(name); strbuf_release(&fullpath); strbuf_release(&class); } @@ -435,25 +408,6 @@ static void ls_tail(void) cgit_print_layout_end(); } -static void ls_tree(const struct object_id *oid, struct walk_tree_context *walk) -{ - struct tree *tree; - struct pathspec paths = { - .nr = 0 - }; - - tree = parse_tree_indirect(oid); - if (!tree) { - cgit_print_error_page(404, "Not Found", "Not a tree object: %s", oid_to_hex(oid)); - return; - } - - ls_head(); - read_tree(the_repository, tree, &paths, ls_item, walk); - ls_flush(walk); - ls_tail(); -} - static int walk_tree(const struct object_id *oid, struct strbuf *base, const char *pathname, unsigned mode, void *data) { @@ -493,18 +447,8 @@ void cgit_print_tree(const char *rev, char *path) { struct object_id oid; struct commit *commit; - int path_len = path ? strlen(path) : 0; - // nowildcard_len matches len so git treats the path as literal rather - // than as a glob. - struct pathspec_item path_items = { - .match = path, - .len = path_len, - .nowildcard_len = path_len - }; - struct pathspec paths = { - .nr = path ? 1 : 0, - .items = &path_items - }; + struct pathspec_item path_item; + struct pathspec paths = { 0 }; struct walk_tree_context walk = { .match_path = path, .state = WALK_LOOKING @@ -513,22 +457,19 @@ void cgit_print_tree(const char *rev, char *path) if (!rev) rev = ctx.qry.head; - if (repo_get_oid(the_repository, rev, &oid)) { - cgit_print_error_page(404, "Not Found", "Bad object id: %s", rev); - return; - } - commit = lookup_commit_reference(the_repository, &oid); - if (!commit || repo_parse_commit(the_repository, commit)) { - cgit_print_error_page(404, "Not Found", "Not a commit: %s", rev); + commit = cgit_get_commit(rev, &oid); + if (!commit) return; - } - walk.rev = xstrdup(rev); + walk.rev = rev; + cgit_literal_pathspec(&paths, &path_item, path); if (!path) { + // The walk is never handed an entry for the top of the tree + // itself, so the listing it would have opened is opened here. cgit_prefix_title("tree"); - ls_tree(get_commit_tree_oid(commit), &walk); - goto cleanup; + walk.state = WALK_LISTING; + ls_head(); } read_tree(the_repository, repo_get_commit_tree(the_repository, commit), &paths, walk_tree, &walk); @@ -542,7 +483,4 @@ void cgit_print_tree(const char *rev, char *path) } // WALK_ERROR_SHOWN is left alone, since the error page print_object // put out is already complete. - -cleanup: - free(walk.rev); } |
