Diff options
Diffstat (limited to '')
-rw-r--r--source/cache.c246
-rw-r--r--source/cache.h12
-rw-r--r--source/cgit.c320
-rw-r--r--source/cgit.h18
-rw-r--r--source/cgit.mk15
-rw-r--r--source/cmd.c71
-rw-r--r--source/cmd.h4
-rw-r--r--source/config.c7
-rw-r--r--source/config.h4
-rw-r--r--source/filter.c120
-rw-r--r--source/html.c136
-rw-r--r--source/html.h10
-rw-r--r--source/parsing.c76
-rw-r--r--source/parsing.h5
-rw-r--r--source/scan-tree.c66
-rw-r--r--source/scan-tree.h1
-rw-r--r--source/shared.c528
-rw-r--r--source/shared.h71
-rw-r--r--source/ui-atom.c33
-rw-r--r--source/ui-blame.c123
-rw-r--r--source/ui-blob.c56
-rw-r--r--source/ui-blob.h6
-rw-r--r--source/ui-clone.c25
-rw-r--r--source/ui-commit.c58
-rw-r--r--source/ui-diff.c126
-rw-r--r--source/ui-diff.h10
-rw-r--r--source/ui-empty.c21
-rw-r--r--source/ui-log.c138
-rw-r--r--source/ui-log.h9
-rw-r--r--source/ui-patch.c94
-rw-r--r--source/ui-plain.c92
-rw-r--r--source/ui-refs.c144
-rw-r--r--source/ui-repolist.c72
-rw-r--r--source/ui-shared.c689
-rw-r--r--source/ui-shared.h77
-rw-r--r--source/ui-snapshot.c206
-rw-r--r--source/ui-snapshot.h14
-rw-r--r--source/ui-ssdiff.c113
-rw-r--r--source/ui-ssdiff.h21
-rw-r--r--source/ui-stats.c186
-rw-r--r--source/ui-stats.h26
-rw-r--r--source/ui-summary.c24
-rw-r--r--source/ui-tag.c32
-rw-r--r--source/ui-tree.c118
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&copy.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 "&lt;";
+ case '>':
+ return "&gt;";
+ case '&':
+ return "&amp;";
+ 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("&gt;");
- else if (c == '<')
- html("&lt;");
- else if (c == '&')
- html("&amp;");
+ 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 = "&lt;";
- else if (*p == '>')
- entity = "&gt;";
- else if (*p == '&')
- entity = "&amp;";
- 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 = "&#x27;";
+ else if (*p == '"')
+ entity = "&quot;";
+ else
+ entity = txt_entity(*p);
+ if (entity) {
html_raw(txt, p - txt);
- if (c == '>')
- html("&gt;");
- else if (c == '<')
- html("&lt;");
- else if (c == '\'')
- html("&#x27;");
- else if (c == '"')
- html("&quot;");
- else if (c == '&')
- html("&amp;");
+ 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&copy.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, &current_repo->desc, &desc_size);
+ cgit_read_first_line(path->buf, &current_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, &notes, 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);
}