diff options
context:
space:
mode:
Diffstat (limited to 'source')
-rw-r--r--source/cache.c475
-rw-r--r--source/cache.h37
-rw-r--r--source/cgit.c1118
-rw-r--r--source/cgit.h404
-rw-r--r--source/cgit.mk150
-rw-r--r--source/cmd.c208
-rw-r--r--source/cmd.h16
-rw-r--r--source/configfile.c90
-rw-r--r--source/configfile.h10
-rw-r--r--source/filter.c457
-rwxr-xr-xsource/gen-version.sh25
-rw-r--r--source/html.c344
-rw-r--r--source/html.h37
-rw-r--r--source/parsing.c227
-rw-r--r--source/scan-tree.c269
-rw-r--r--source/scan-tree.h2
-rw-r--r--source/shared.c595
-rw-r--r--source/ui-atom.c159
-rw-r--r--source/ui-atom.h6
-rw-r--r--source/ui-blame.c317
-rw-r--r--source/ui-blame.h6
-rw-r--r--source/ui-blob.c184
-rw-r--r--source/ui-blob.h8
-rw-r--r--source/ui-clone.c138
-rw-r--r--source/ui-clone.h8
-rw-r--r--source/ui-commit.c150
-rw-r--r--source/ui-commit.h6
-rw-r--r--source/ui-diff.c503
-rw-r--r--source/ui-diff.h15
-rw-r--r--source/ui-log.c553
-rw-r--r--source/ui-log.h9
-rw-r--r--source/ui-patch.c100
-rw-r--r--source/ui-patch.h7
-rw-r--r--source/ui-plain.c209
-rw-r--r--source/ui-plain.h6
-rw-r--r--source/ui-refs.c224
-rw-r--r--source/ui-refs.h8
-rw-r--r--source/ui-repolist.c379
-rw-r--r--source/ui-repolist.h7
-rw-r--r--source/ui-shared.c1265
-rw-r--r--source/ui-shared.h88
-rw-r--r--source/ui-snapshot.c318
-rw-r--r--source/ui-snapshot.h7
-rw-r--r--source/ui-ssdiff.c420
-rw-r--r--source/ui-ssdiff.h25
-rw-r--r--source/ui-stats.c435
-rw-r--r--source/ui-stats.h28
-rw-r--r--source/ui-summary.c148
-rw-r--r--source/ui-summary.h7
-rw-r--r--source/ui-tag.c122
-rw-r--r--source/ui-tag.h6
-rw-r--r--source/ui-tree.c407
-rw-r--r--source/ui-tree.h6
53 files changed, 10748 insertions, 0 deletions
diff --git a/source/cache.c b/source/cache.c
new file mode 100644
index 0000000..e70af13
--- /dev/null
+++ b/source/cache.c
@@ -0,0 +1,475 @@
+/* cache.c: cache management
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ *
+ *
+ * The cache is just a directory structure where each file is a cache slot,
+ * and each filename is based on the hash of some key (e.g. the cgit url).
+ * Each file contains the full key followed by the cached content for that
+ * key.
+ *
+ */
+
+#include "cgit.h"
+#include "cache.h"
+#include "html.h"
+#ifdef HAVE_LINUX_SENDFILE
+#include <sys/sendfile.h>
+#endif
+
+#define CACHE_BUFSIZE (1024 * 4)
+
+struct cache_slot {
+ const char *key;
+ size_t keylen;
+ int ttl;
+ cache_fill_fn fn;
+ int cache_fd;
+ int lock_fd;
+ int stdout_fd;
+ const char *cache_name;
+ const char *lock_name;
+ int match;
+ struct stat cache_st;
+ int bufsize;
+ char buf[CACHE_BUFSIZE];
+};
+
+/* Open an existing cache slot and fill the cache buffer with
+ * (part of) the content of the cache file. Return 0 on success
+ * and errno otherwise.
+ */
+static int open_slot(struct cache_slot *slot)
+{
+ char *bufz;
+ ssize_t bufkeylen = -1;
+
+ slot->cache_fd = open(slot->cache_name, O_RDONLY);
+ if (slot->cache_fd == -1)
+ return errno;
+
+ if (fstat(slot->cache_fd, &slot->cache_st))
+ return errno;
+
+ slot->bufsize = xread(slot->cache_fd, slot->buf, sizeof(slot->buf));
+ if (slot->bufsize < 0)
+ return errno;
+
+ bufz = memchr(slot->buf, 0, slot->bufsize);
+ if (bufz)
+ bufkeylen = bufz - slot->buf;
+
+ if (slot->key)
+ slot->match = bufkeylen == slot->keylen &&
+ !memcmp(slot->key, slot->buf, bufkeylen + 1);
+
+ return 0;
+}
+
+/* Close the active cache slot */
+static int close_slot(struct cache_slot *slot)
+{
+ int err = 0;
+ if (slot->cache_fd > 0) {
+ if (close(slot->cache_fd))
+ err = errno;
+ else
+ slot->cache_fd = -1;
+ }
+ return err;
+}
+
+/* Print the content of the active cache slot (but skip the key). */
+static int print_slot(struct cache_slot *slot)
+{
+ off_t off;
+#ifdef HAVE_LINUX_SENDFILE
+ off_t size;
+#endif
+
+ off = slot->keylen + 1;
+
+#ifdef HAVE_LINUX_SENDFILE
+ size = slot->cache_st.st_size;
+
+ do {
+ ssize_t ret;
+ ret = sendfile(STDOUT_FILENO, slot->cache_fd, &off, size - off);
+ if (ret < 0) {
+ if (errno == EAGAIN || errno == EINTR)
+ continue;
+ /* Fall back to read/write on EINVAL or ENOSYS */
+ if (errno == EINVAL || errno == ENOSYS)
+ break;
+ return errno;
+ }
+ if (off == size)
+ return 0;
+ } while (1);
+#endif
+
+ if (lseek(slot->cache_fd, off, SEEK_SET) != off)
+ return errno;
+
+ do {
+ ssize_t ret;
+ ret = xread(slot->cache_fd, slot->buf, sizeof(slot->buf));
+ if (ret < 0)
+ return errno;
+ if (ret == 0)
+ return 0;
+ if (write_in_full(STDOUT_FILENO, slot->buf, ret) < 0)
+ return errno;
+ } while (1);
+}
+
+/* Check if the slot has expired */
+static int is_expired(struct cache_slot *slot)
+{
+ if (slot->ttl < 0)
+ return 0;
+ else
+ return slot->cache_st.st_mtime + slot->ttl * 60 < time(NULL);
+}
+
+/* Check if the slot has been modified since we opened it.
+ * NB: If stat() fails, we pretend the file is modified.
+ */
+static int is_modified(struct cache_slot *slot)
+{
+ struct stat st;
+
+ if (stat(slot->cache_name, &st))
+ return 1;
+ return (st.st_ino != slot->cache_st.st_ino ||
+ st.st_mtime != slot->cache_st.st_mtime ||
+ st.st_size != slot->cache_st.st_size);
+}
+
+/* Close an open lockfile */
+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;
+}
+
+/* Create a lockfile used to store the generated content for a cache
+ * slot, and write the slot key + \0 into it.
+ * Returns 0 on success and errno otherwise.
+ */
+static int lock_slot(struct cache_slot *slot)
+{
+ struct flock lock = {
+ .l_type = F_WRLCK,
+ .l_whence = SEEK_SET,
+ .l_start = 0,
+ .l_len = 0,
+ };
+
+ slot->lock_fd = open(slot->lock_name, O_RDWR | O_CREAT,
+ S_IRUSR | S_IWUSR);
+ if (slot->lock_fd == -1)
+ return errno;
+ if (fcntl(slot->lock_fd, F_SETLK, &lock) < 0) {
+ int saved_errno = errno;
+ close(slot->lock_fd);
+ slot->lock_fd = -1;
+ return saved_errno;
+ }
+ if (ftruncate(slot->lock_fd, 0) < 0)
+ return errno;
+ if (xwrite(slot->lock_fd, slot->key, slot->keylen + 1) < 0)
+ return errno;
+ return 0;
+}
+
+/* Release the current lockfile. If `replace_old_slot` is set the
+ * lockfile replaces the old cache slot, otherwise the lockfile is
+ * just deleted.
+ */
+static int unlock_slot(struct cache_slot *slot, int replace_old_slot)
+{
+ int err;
+
+ if (replace_old_slot)
+ err = rename(slot->lock_name, slot->cache_name);
+ else
+ err = unlink(slot->lock_name);
+
+ /* Restore stdout and close the temporary FD. */
+ if (slot->stdout_fd >= 0) {
+ dup2(slot->stdout_fd, STDOUT_FILENO);
+ close(slot->stdout_fd);
+ slot->stdout_fd = -1;
+ }
+
+ if (err)
+ return errno;
+
+ return 0;
+}
+
+/* Generate the content for the current cache slot by redirecting
+ * stdout to the lock-fd and invoking the callback function
+ */
+static int fill_slot(struct cache_slot *slot)
+{
+ /* Preserve stdout */
+ slot->stdout_fd = dup(STDOUT_FILENO);
+ if (slot->stdout_fd == -1)
+ return errno;
+
+ /* Redirect stdout to lockfile */
+ if (dup2(slot->lock_fd, STDOUT_FILENO) == -1)
+ return errno;
+
+ /* Generate cache content */
+ slot->fn();
+
+ /* Make sure any buffered data is flushed to the file */
+ if (fflush(stdout))
+ return errno;
+
+ /* update stat info */
+ if (fstat(slot->lock_fd, &slot->cache_st))
+ return errno;
+
+ return 0;
+}
+
+/* Crude implementation of 32-bit FNV-1 hash algorithm,
+ * see http://www.isthe.com/chongo/tech/comp/fnv/ for details
+ * about the magic numbers.
+ */
+#define FNV_OFFSET 0x811c9dc5
+#define FNV_PRIME 0x01000193
+
+unsigned long 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++;
+ }
+ return h;
+}
+
+static int process_slot(struct cache_slot *slot)
+{
+ int err;
+
+ err = open_slot(slot);
+ if (!err && slot->match) {
+ if (is_expired(slot)) {
+ if (!lock_slot(slot)) {
+ /* If the cachefile has been replaced between
+ * `open_slot` and `lock_slot`, we'll just
+ * serve the stale content from the original
+ * cachefile. This way we avoid pruning the
+ * newly generated slot. The same code-path
+ * is chosen if fill_slot() fails for some
+ * reason.
+ *
+ * TODO? check if the new slot contains the
+ * same key as the old one, since we would
+ * prefer to serve the newest content.
+ * This will require us to open yet another
+ * file-descriptor and read and compare the
+ * key from the new file, so for now we're
+ * lazy and just ignore the new file.
+ */
+ if (is_modified(slot) || fill_slot(slot)) {
+ unlock_slot(slot, 0);
+ close_lock(slot);
+ } else {
+ close_slot(slot);
+ unlock_slot(slot, 1);
+ slot->cache_fd = slot->lock_fd;
+ }
+ }
+ }
+ if ((err = print_slot(slot)) != 0) {
+ cache_log("[cgit] error printing cache %s: %s (%d)\n",
+ slot->cache_name,
+ strerror(err),
+ err);
+ }
+ close_slot(slot);
+ return err;
+ }
+
+ /* If the cache slot does not exist (or its key doesn't match the
+ * current key), lets try to create a new cache slot for this
+ * request. If this fails (for whatever reason), lets just generate
+ * the content without caching it and fool the caller to believe
+ * everything worked out (but print a warning on stdout).
+ */
+
+ close_slot(slot);
+ if ((err = lock_slot(slot)) != 0) {
+ cache_log("[cgit] Unable to lock slot %s: %s (%d)\n",
+ slot->lock_name, strerror(err), err);
+ slot->fn();
+ return 0;
+ }
+
+ if ((err = fill_slot(slot)) != 0) {
+ cache_log("[cgit] Unable to fill slot %s: %s (%d)\n",
+ slot->lock_name, strerror(err), err);
+ unlock_slot(slot, 0);
+ close_lock(slot);
+ slot->fn();
+ return 0;
+ }
+ // We've got a valid cache slot in the lock file, which
+ // is about to replace the old cache slot. But if we
+ // release the lockfile and then try to open the new cache
+ // slot, we might get a race condition with a concurrent
+ // writer for the same cache slot (with a different key).
+ // Lets avoid such a race by just printing the content of
+ // the lock file.
+ slot->cache_fd = slot->lock_fd;
+ unlock_slot(slot, 1);
+ if ((err = print_slot(slot)) != 0) {
+ cache_log("[cgit] error printing cache %s: %s (%d)\n",
+ slot->cache_name,
+ strerror(err),
+ err);
+ }
+ close_slot(slot);
+ return err;
+}
+
+/* Print cached content to stdout, generate the content if necessary. */
+int cache_process(int size, const char *path, const char *key, int ttl,
+ cache_fill_fn fn)
+{
+ unsigned long hash;
+ int i;
+ struct strbuf filename = STRBUF_INIT;
+ struct strbuf lockname = STRBUF_INIT;
+ struct cache_slot slot;
+ int result;
+
+ /* If the cache is disabled, just generate the content */
+ if (size <= 0 || ttl == 0) {
+ fn();
+ return 0;
+ }
+
+ /* Verify input, calculate filenames */
+ if (!path) {
+ cache_log("[cgit] Cache path not specified, caching is disabled\n");
+ fn();
+ return 0;
+ }
+ if (!key)
+ key = "";
+ hash = hash_str(key) % size;
+ strbuf_addstr(&filename, path);
+ strbuf_ensure_end(&filename, '/');
+ for (i = 0; i < 8; i++) {
+ strbuf_addf(&filename, "%x", (unsigned char)(hash & 0xf));
+ hash >>= 4;
+ }
+ strbuf_addbuf(&lockname, &filename);
+ strbuf_addstr(&lockname, ".lock");
+ slot.fn = fn;
+ slot.ttl = ttl;
+ slot.stdout_fd = -1;
+ slot.cache_name = filename.buf;
+ slot.lock_name = lockname.buf;
+ slot.key = key;
+ slot.keylen = strlen(key);
+ result = process_slot(&slot);
+
+ strbuf_release(&filename);
+ strbuf_release(&lockname);
+ return result;
+}
+
+/* Return a strftime formatted date/time
+ * NB: the result from this function is to shared memory
+ */
+static char *sprintftime(const char *format, time_t time)
+{
+ static char buf[64];
+ struct tm tm;
+
+ if (!time)
+ return NULL;
+ gmtime_r(&time, &tm);
+ strftime(buf, sizeof(buf)-1, format, &tm);
+ return buf;
+}
+
+int cache_ls(const char *path)
+{
+ DIR *dir;
+ struct dirent *ent;
+ int err = 0;
+ struct cache_slot slot = { NULL };
+ struct strbuf fullname = STRBUF_INIT;
+ size_t prefixlen;
+
+ if (!path) {
+ cache_log("[cgit] cache path not specified\n");
+ return -1;
+ }
+ dir = opendir(path);
+ if (!dir) {
+ err = errno;
+ cache_log("[cgit] unable to open path %s: %s (%d)\n",
+ path, strerror(err), err);
+ return err;
+ }
+ strbuf_addstr(&fullname, path);
+ strbuf_ensure_end(&fullname, '/');
+ prefixlen = fullname.len;
+ while ((ent = readdir(dir)) != NULL) {
+ if (strlen(ent->d_name) != 8)
+ continue;
+ strbuf_setlen(&fullname, prefixlen);
+ strbuf_addstr(&fullname, ent->d_name);
+ slot.cache_name = fullname.buf;
+ if ((err = open_slot(&slot)) != 0) {
+ cache_log("[cgit] unable to open path %s: %s (%d)\n",
+ fullname.buf, strerror(err), err);
+ continue;
+ }
+ htmlf("%s %s %10"PRIuMAX" %s\n",
+ fullname.buf,
+ sprintftime("%Y-%m-%d %H:%M:%S",
+ slot.cache_st.st_mtime),
+ (uintmax_t)slot.cache_st.st_size,
+ slot.buf);
+ close_slot(&slot);
+ }
+ closedir(dir);
+ strbuf_release(&fullname);
+ return 0;
+}
+
+/* Print a message to stdout */
+void cache_log(const char *format, ...)
+{
+ va_list args;
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+}
+
diff --git a/source/cache.h b/source/cache.h
new file mode 100644
index 0000000..470da4f
--- /dev/null
+++ b/source/cache.h
@@ -0,0 +1,37 @@
+/*
+ * Since git has it's own cache.h which we include,
+ * lets test on CGIT_CACHE_H to avoid confusion
+ */
+
+#ifndef CGIT_CACHE_H
+#define CGIT_CACHE_H
+
+typedef void (*cache_fill_fn)(void);
+
+
+/* Print cached content to stdout, generate the content if necessary.
+ *
+ * Parameters
+ * size max number of cache files
+ * path directory used to store cache files
+ * key the key used to lookup cache files
+ * ttl max cache time in seconds for this key
+ * fn content generator function for this key
+ *
+ * Return value
+ * 0 indicates success, everything else is an error
+ */
+extern int cache_process(int size, const char *path, const char *key, int ttl,
+ cache_fill_fn fn);
+
+
+/* List info about all cache entries on stdout */
+extern int cache_ls(const char *path);
+
+/* Print a message to stdout */
+__attribute__((format (printf,1,2)))
+extern void cache_log(const char *format, ...);
+
+extern unsigned long hash_str(const char *str);
+
+#endif /* CGIT_CACHE_H */
diff --git a/source/cgit.c b/source/cgit.c
new file mode 100644
index 0000000..ca318e8
--- /dev/null
+++ b/source/cgit.c
This diff is too large to be rendered inline. View it on its own page.
diff --git a/source/cgit.h b/source/cgit.h
new file mode 100644
index 0000000..7d7ece7
--- /dev/null
+++ b/source/cgit.h
@@ -0,0 +1,404 @@
+#ifndef CGIT_H
+#define CGIT_H
+
+#include <stdbool.h>
+
+#include <git-compat-util.h>
+
+#include <archive.h>
+#include <commit.h>
+#include <diffcore.h>
+#include <diff.h>
+#include <environment.h>
+#include <graph.h>
+#include <grep.h>
+#include <hex.h>
+#include <log-tree.h>
+#include <notes.h>
+#include <object.h>
+#include <object-name.h>
+#include <odb.h>
+#include <path.h>
+#include <refs.h>
+#include <revision.h>
+#include <setup.h>
+#include <string-list.h>
+#include <strvec.h>
+#include <tag.h>
+#include <tree.h>
+#include <utf8.h>
+#include <wrapper.h>
+#include <xdiff-interface.h>
+#include <xdiff/xdiff.h>
+
+/* Add isgraph(x) to Git's sane ctype support (see git-compat-util.h) */
+#undef isgraph
+#define isgraph(x) (isprint((x)) && !isspace((x)))
+
+
+/*
+ * Limits used for relative dates
+ */
+#define TM_MIN 60
+#define TM_HOUR (TM_MIN * 60)
+#define TM_DAY (TM_HOUR * 24)
+#define TM_WEEK (TM_DAY * 7)
+#define TM_YEAR (TM_DAY * 365)
+#define TM_MONTH (TM_YEAR / 12.0)
+
+
+/*
+ * Default encoding
+ */
+#define PAGE_ENCODING "UTF-8"
+
+#define BIT(x) (1U << (x))
+
+typedef void (*configfn)(const char *name, const char *value);
+typedef void (*filepair_fn)(struct diff_filepair *pair);
+typedef void (*linediff_fn)(char *line, int len);
+
+typedef enum {
+ DIFF_UNIFIED, DIFF_SSDIFF, DIFF_STATONLY
+} diff_type;
+
+typedef enum {
+ ABOUT, COMMIT, SOURCE, EMAIL, AUTH, OWNER
+} filter_type;
+
+struct cgit_filter {
+ int (*open)(struct cgit_filter *, va_list ap);
+ int (*close)(struct cgit_filter *);
+ void (*fprintfp)(struct cgit_filter *, FILE *, const char *prefix);
+ void (*cleanup)(struct cgit_filter *);
+ int argument_count;
+};
+
+struct cgit_exec_filter {
+ struct cgit_filter base;
+ char *cmd;
+ char **argv;
+ int old_stdout;
+ int pid;
+};
+
+struct cgit_repo {
+ char *url;
+ char *name;
+ char *path;
+ char *desc;
+ char *extra_head_content;
+ char *owner;
+ char *homepage;
+ char *defbranch;
+ char *module_link;
+ struct string_list readme;
+ char *section;
+ char *clone_url;
+ char *logo;
+ char *logo_link;
+ char *snapshot_prefix;
+ int snapshots;
+ int enable_blame;
+ int enable_commit_graph;
+ int enable_follow_links;
+ int enable_log_filecount;
+ int enable_log_linecount;
+ int enable_remote_branches;
+ int enable_subject_links;
+ int enable_html_serving;
+ int max_stats;
+ int branch_sort;
+ int commit_sort;
+ time_t mtime;
+ struct cgit_filter *about_filter;
+ struct cgit_filter *commit_filter;
+ struct cgit_filter *source_filter;
+ struct cgit_filter *email_filter;
+ struct cgit_filter *owner_filter;
+ struct string_list submodules;
+ int hide;
+ int ignore;
+};
+
+struct cgit_repolist {
+ int length;
+ int count;
+ struct cgit_repo *repos;
+};
+
+struct commitinfo {
+ struct commit *commit;
+ char *author;
+ char *author_email;
+ unsigned long author_date;
+ int author_tz;
+ char *committer;
+ char *committer_email;
+ unsigned long committer_date;
+ int committer_tz;
+ char *subject;
+ char *msg;
+ char *msg_encoding;
+};
+
+struct taginfo {
+ char *tagger;
+ char *tagger_email;
+ unsigned long tagger_date;
+ int tagger_tz;
+ 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_symref;
+ int has_oid;
+ int has_difftype;
+ char *raw;
+ char *repo;
+ char *page;
+ char *search;
+ char *grep;
+ char *head;
+ char *oid;
+ char *oid2;
+ char *path;
+ char *name;
+ char *url;
+ char *period;
+ int ofs;
+ int nohead;
+ char *sort;
+ int showmsg;
+ diff_type difftype;
+ int show_all;
+ int context;
+ int ignorews;
+ int follow;
+ char *vpath;
+};
+
+struct cgit_config {
+ char *agefile;
+ char *cache_root;
+ char *clone_prefix;
+ char *clone_url;
+ char *favicon;
+ char *footer;
+ char *head_include;
+ char *header;
+ char *logo;
+ char *logo_link;
+ char *mimetype_file;
+ char *module_link;
+ char *project_list;
+ struct string_list readme;
+ struct string_list css;
+ char *robots;
+ char *root_title;
+ char *root_desc;
+ char *root_readme;
+ char *script_name;
+ char *section;
+ char *repository_sort;
+ char *virtual_root; /* Always ends with '/'. */
+ char *strict_export;
+ int cache_size;
+ int cache_dynamic_ttl;
+ int cache_max_create_time;
+ int cache_repo_ttl;
+ int cache_root_ttl;
+ int cache_scanrc_ttl;
+ int cache_static_ttl;
+ int cache_about_ttl;
+ int cache_snapshot_ttl;
+ int case_sensitive_sort;
+ int embedded;
+ int enable_filter_overrides;
+ int enable_follow_links;
+ int enable_http_clone;
+ int enable_index_links;
+ int enable_index_owner;
+ int enable_blame;
+ int enable_commit_graph;
+ int enable_log_filecount;
+ int enable_log_linecount;
+ int enable_remote_branches;
+ int enable_subject_links;
+ int enable_html_serving;
+ int enable_tree_linenumbers;
+ int enable_git_config;
+ int local_time;
+ int max_atom_items;
+ int max_repo_count;
+ int max_commit_count;
+ int max_lock_attempts;
+ int max_msg_len;
+ int max_repodesc_len;
+ int max_blob_size;
+ int max_stats;
+ int noplainemail;
+ int noheader;
+ int renamelimit;
+ int remove_suffix;
+ int scan_hidden_path;
+ int section_from_path;
+ int snapshots;
+ int section_sort;
+ int summary_branches;
+ int summary_log;
+ int summary_tags;
+ diff_type difftype;
+ int branch_sort;
+ int commit_sort;
+ struct string_list mimetypes;
+ struct string_list js;
+ struct cgit_filter *about_filter;
+ struct cgit_filter *commit_filter;
+ struct cgit_filter *source_filter;
+ struct cgit_filter *email_filter;
+ struct cgit_filter *owner_filter;
+ struct cgit_filter *auth_filter;
+};
+
+struct cgit_page {
+ time_t modified;
+ time_t expires;
+ size_t size;
+ const char *mimetype;
+ const char *charset;
+ const char *filename;
+ const char *etag;
+ const char *title;
+ int status;
+ const char *statusmsg;
+};
+
+struct cgit_environment {
+ const char *cgit_config;
+ const char *http_host;
+ const char *https;
+ const char *no_http;
+ 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;
+ const char *http_referer;
+ unsigned int content_length;
+ int authenticated;
+};
+
+struct cgit_context {
+ struct cgit_environment env;
+ struct cgit_query qry;
+ struct cgit_config cfg;
+ struct cgit_repo *repo;
+ struct cgit_page page;
+};
+
+typedef int (*write_archive_fn_t)(const char *, const char *);
+
+struct cgit_snapshot_format {
+ const char *suffix;
+ const char *mimetype;
+ write_archive_fn_t write_func;
+};
+
+extern const char *cgit_version;
+
+extern struct cgit_repolist cgit_repolist;
+extern struct cgit_context ctx;
+extern const struct cgit_snapshot_format cgit_snapshot_formats[];
+
+extern char *cgit_default_repo_desc;
+extern struct cgit_repo *cgit_add_repo(const char *url);
+extern struct cgit_repo *cgit_get_repoinfo(const char *url);
+extern void cgit_repo_config(struct cgit_repo *repo, const char *name,
+ const char *value);
+
+extern int chk_zero(int result, char *msg);
+extern int chk_positive(int result, char *msg);
+extern int chk_non_negative(int result, char *msg);
+
+extern char *trim_end(const char *str, char c);
+extern char *ensure_end(const char *str, char c);
+
+extern void strbuf_ensure_end(struct strbuf *sb, char c);
+
+extern void cgit_add_ref(struct reflist *list, struct refinfo *ref);
+extern void cgit_free_reflist_inner(struct reflist *list);
+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);
+
+void cgit_diff_tree_cb(struct diff_queue_struct *q,
+ struct diff_options *options, void *data);
+
+extern int cgit_diff_files(const struct object_id *old_oid,
+ const struct object_id *new_oid,
+ unsigned long *old_size, unsigned long *new_size,
+ int *binary, int context, int ignorews,
+ linediff_fn fn);
+
+extern void cgit_diff_tree(const struct object_id *old_oid,
+ const struct object_id *new_oid,
+ filepair_fn fn, const char *prefix, int ignorews);
+
+extern void cgit_diff_commit(struct commit *commit, filepair_fn fn,
+ const char *prefix);
+
+__attribute__((format (printf,1,2)))
+extern char *fmt(const char *format,...);
+
+__attribute__((format (printf,1,2)))
+extern char *fmtalloc(const char *format,...);
+
+extern struct commitinfo *cgit_parse_commit(struct commit *commit);
+extern struct taginfo *cgit_parse_tag(struct tag *tag);
+extern void cgit_parse_url(const char *url);
+
+extern const char *cgit_repobasename(const char *reponame);
+
+extern int cgit_parse_snapshots_mask(const char *str);
+extern const struct object_id *cgit_snapshot_get_sig(const char *ref,
+ const struct cgit_snapshot_format *f);
+extern const unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f);
+
+extern int cgit_open_filter(struct cgit_filter *filter, ...);
+extern int cgit_close_filter(struct cgit_filter *filter);
+extern void cgit_fprintf_filter(struct cgit_filter *filter, FILE *f, const char *prefix);
+extern void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv);
+extern struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype);
+extern void cgit_cleanup_filters(void);
+extern void cgit_init_filters(void);
+
+extern void cgit_prepare_repo_env(struct cgit_repo * repo);
+
+extern int read_first_line(const char *path, char **buf, size_t *size);
+
+extern char *strdup_first_line(const char *txt);
+
+extern char *expand_macros(const char *txt);
+
+extern char *get_mimetype_for_filename(const char *filename);
+
+#endif /* CGIT_H */
diff --git a/source/cgit.mk b/source/cgit.mk
new file mode 100644
index 0000000..d012349
--- /dev/null
+++ b/source/cgit.mk
@@ -0,0 +1,150 @@
+# This Makefile runs inside the bundled Git tree (libraries/git) so that it
+# can reuse Git's build variables and platform detection. The top-level
+# Makefile invokes it as:
+#
+# make -C libraries/git -f ../../source/cgit.mk ../../build/cgit
+#
+# so every path back into the project is prefixed with "../../": sources come
+# from ../../source and all build output goes to ../../build.
+include Makefile
+
+# Locations relative to libraries/git, where this file is run.
+CGIT_PREFIX = ../../
+CGIT_SRC = $(CGIT_PREFIX)source/
+CGIT_BUILD = $(CGIT_PREFIX)build/
+
+-include $(CGIT_PREFIX)cgit.conf
+
+# The CGIT_* variables are inherited from the top-level Makefile.
+
+$(CGIT_BUILD)VERSION: force-version
+ @mkdir -p $(CGIT_BUILD)
+ @cd $(CGIT_PREFIX) && '$(SHELL_PATH_SQ)' source/gen-version.sh "$(CGIT_VERSION)" build/VERSION
+-include $(CGIT_BUILD)VERSION
+.PHONY: force-version
+
+# CGIT_CFLAGS is tracked separately so that changing it does not force a
+# rebuild of Git itself.
+CGIT_CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"'
+CGIT_CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"'
+CGIT_CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"'
+
+PKG_CONFIG ?= pkg-config
+
+ifdef NO_C99_FORMAT
+ CFLAGS += -DNO_C99_FORMAT
+endif
+
+ifdef NO_LUA
+ LUA_MESSAGE := linking without specified Lua support
+ CGIT_CFLAGS += -DNO_LUA
+else
+ifeq ($(LUA_PKGCONFIG),)
+ LUA_PKGCONFIG := $(shell for pc in luajit lua lua5.2 lua5.1; do \
+ $(PKG_CONFIG) --exists $$pc 2>/dev/null && echo $$pc && break; \
+ done)
+ LUA_MODE := autodetected
+else
+ LUA_MODE := specified
+endif
+ifneq ($(LUA_PKGCONFIG),)
+ LUA_MESSAGE := linking with $(LUA_MODE) $(LUA_PKGCONFIG)
+ LUA_LIBS := $(shell $(PKG_CONFIG) --libs $(LUA_PKGCONFIG) 2>/dev/null)
+ LUA_CFLAGS := $(shell $(PKG_CONFIG) --cflags $(LUA_PKGCONFIG) 2>/dev/null)
+ CGIT_LIBS += $(LUA_LIBS)
+ CGIT_CFLAGS += $(LUA_CFLAGS)
+else
+ LUA_MESSAGE := linking without autodetected Lua support
+ NO_LUA := YesPlease
+ CGIT_CFLAGS += -DNO_LUA
+endif
+
+endif
+
+# Add -ldl to linker flags on systems that commonly use GNU libc.
+ifneq (,$(filter $(uname_S),Linux GNU GNU/kFreeBSD))
+ CGIT_LIBS += -ldl
+endif
+
+# glibc 2.1+ offers sendfile which the most common C library on Linux
+ifeq ($(uname_S),Linux)
+ HAVE_LINUX_SENDFILE = YesPlease
+endif
+
+ifdef HAVE_LINUX_SENDFILE
+ CGIT_CFLAGS += -DHAVE_LINUX_SENDFILE
+endif
+
+CGIT_OBJ_NAMES += cgit.o
+CGIT_OBJ_NAMES += cache.o
+CGIT_OBJ_NAMES += cmd.o
+CGIT_OBJ_NAMES += configfile.o
+CGIT_OBJ_NAMES += filter.o
+CGIT_OBJ_NAMES += html.o
+CGIT_OBJ_NAMES += parsing.o
+CGIT_OBJ_NAMES += scan-tree.o
+CGIT_OBJ_NAMES += shared.o
+CGIT_OBJ_NAMES += ui-atom.o
+CGIT_OBJ_NAMES += ui-blame.o
+CGIT_OBJ_NAMES += ui-blob.o
+CGIT_OBJ_NAMES += ui-clone.o
+CGIT_OBJ_NAMES += ui-commit.o
+CGIT_OBJ_NAMES += ui-diff.o
+CGIT_OBJ_NAMES += ui-log.o
+CGIT_OBJ_NAMES += ui-patch.o
+CGIT_OBJ_NAMES += ui-plain.o
+CGIT_OBJ_NAMES += ui-refs.o
+CGIT_OBJ_NAMES += ui-repolist.o
+CGIT_OBJ_NAMES += ui-shared.o
+CGIT_OBJ_NAMES += ui-snapshot.o
+CGIT_OBJ_NAMES += ui-ssdiff.o
+CGIT_OBJ_NAMES += ui-stats.o
+CGIT_OBJ_NAMES += ui-summary.o
+CGIT_OBJ_NAMES += ui-tag.o
+CGIT_OBJ_NAMES += ui-tree.o
+
+CGIT_OBJS := $(addprefix $(CGIT_BUILD),$(CGIT_OBJ_NAMES))
+
+# Only cgit.c references CGIT_VERSION, so only its objects rebuild when the
+# version changes.
+CGIT_VERSION_OBJS := $(addprefix $(CGIT_BUILD),cgit.o cgit.sp)
+$(CGIT_VERSION_OBJS): $(CGIT_BUILD)VERSION
+$(CGIT_VERSION_OBJS): EXTRA_CPPFLAGS = \
+ -DCGIT_VERSION='"$(CGIT_VERSION)"'
+
+# Git handles dependencies using ":=" so dependencies in CGIT_OBJS are not
+# handled by that and we must handle them ourselves.
+cgit_dep_files := $(foreach f,$(CGIT_OBJS),$(dir $f).depend/$(notdir $f).d)
+cgit_dep_files_present := $(wildcard $(cgit_dep_files))
+ifneq ($(cgit_dep_files_present),)
+include $(cgit_dep_files_present)
+endif
+
+ifeq ($(wildcard $(CGIT_BUILD).depend),)
+missing_dep_dirs += $(CGIT_BUILD).depend
+endif
+
+$(CGIT_BUILD).depend:
+ @mkdir -p $@
+
+$(CGIT_BUILD)CGIT-CFLAGS: FORCE
+ @mkdir -p $(CGIT_BUILD)
+ @FLAGS='$(subst ','\'',$(CGIT_CFLAGS))'; \
+ if test x"$$FLAGS" != x"`cat $(CGIT_BUILD)CGIT-CFLAGS 2>/dev/null`" ; then \
+ echo 1>&2 " * new CGit build flags"; \
+ echo "$$FLAGS" >$(CGIT_BUILD)CGIT-CFLAGS; \
+ fi
+
+$(CGIT_OBJS): $(CGIT_BUILD)%.o: $(CGIT_SRC)%.c GIT-CFLAGS $(CGIT_BUILD)CGIT-CFLAGS $(missing_dep_dirs)
+ $(QUIET_CC)$(CC) -o $@ -c $(dep_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $(CGIT_CFLAGS) $<
+
+$(CGIT_BUILD)cgit: $(CGIT_OBJS) GIT-LDFLAGS $(GITLIBS)
+ @echo 1>&1 " * $(LUA_MESSAGE)"
+ $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS) $(CGIT_LIBS)
+
+CGIT_SP_OBJS := $(patsubst %.o,%.sp,$(CGIT_OBJS))
+
+$(CGIT_SP_OBJS): $(CGIT_BUILD)%.sp: $(CGIT_SRC)%.c GIT-CFLAGS $(CGIT_BUILD)CGIT-CFLAGS FORCE
+ $(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $(CGIT_CFLAGS) $(SPARSE_FLAGS) $<
+
+cgit-sparse: $(CGIT_SP_OBJS)
diff --git a/source/cmd.c b/source/cmd.c
new file mode 100644
index 0000000..0eb75b1
--- /dev/null
+++ b/source/cmd.c
@@ -0,0 +1,208 @@
+/* cmd.c: the cgit command dispatcher
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "cmd.h"
+#include "cache.h"
+#include "ui-shared.h"
+#include "ui-atom.h"
+#include "ui-blame.h"
+#include "ui-blob.h"
+#include "ui-clone.h"
+#include "ui-commit.h"
+#include "ui-diff.h"
+#include "ui-log.h"
+#include "ui-patch.h"
+#include "ui-plain.h"
+#include "ui-refs.h"
+#include "ui-repolist.h"
+#include "ui-snapshot.h"
+#include "ui-stats.h"
+#include "ui-summary.h"
+#include "ui-tag.h"
+#include "ui-tree.h"
+
+static void HEAD_fn(void)
+{
+ cgit_clone_head();
+}
+
+static void atom_fn(void)
+{
+ cgit_print_atom(ctx.qry.head, ctx.qry.path, ctx.cfg.max_atom_items);
+}
+
+static void about_fn(void)
+{
+ if (ctx.repo) {
+ size_t path_info_len = ctx.env.path_info ? strlen(ctx.env.path_info) : 0;
+ if (!ctx.qry.path &&
+ ctx.qry.url[strlen(ctx.qry.url) - 1] != '/' &&
+ (!path_info_len || ctx.env.path_info[path_info_len - 1] != '/')) {
+ char *currenturl = cgit_currenturl();
+ char *redirect = fmtalloc("%s/", currenturl);
+ cgit_redirect(redirect, true);
+ free(currenturl);
+ free(redirect);
+ } else if (ctx.repo->readme.nr)
+ cgit_print_repo_readme(ctx.qry.path);
+ else if (ctx.repo->homepage)
+ cgit_redirect(ctx.repo->homepage, false);
+ else {
+ char *currenturl = cgit_currenturl();
+ char *redirect = fmtalloc("%s../", currenturl);
+ cgit_redirect(redirect, false);
+ free(currenturl);
+ free(redirect);
+ }
+ } else
+ cgit_print_site_readme();
+}
+
+static void blame_fn(void)
+{
+ if (ctx.repo->enable_blame)
+ cgit_print_blame();
+ else
+ cgit_print_error_page(403, "Forbidden", "Blame is disabled");
+}
+
+static void blob_fn(void)
+{
+ cgit_print_blob(ctx.qry.oid, ctx.qry.path, ctx.qry.head, 0);
+}
+
+static void commit_fn(void)
+{
+ cgit_print_commit(ctx.qry.oid, ctx.qry.path);
+}
+
+static void diff_fn(void)
+{
+ cgit_print_diff(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path, 1, 0);
+}
+
+static void rawdiff_fn(void)
+{
+ cgit_print_diff(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path, 1, 1);
+}
+
+static void info_fn(void)
+{
+ cgit_clone_info();
+}
+
+static void log_fn(void)
+{
+ cgit_print_log(ctx.qry.oid, ctx.qry.ofs, ctx.cfg.max_commit_count,
+ ctx.qry.grep, ctx.qry.search, ctx.qry.path, 1,
+ ctx.repo->enable_commit_graph,
+ ctx.repo->commit_sort);
+}
+
+static void ls_cache_fn(void)
+{
+ ctx.page.mimetype = "text/plain";
+ ctx.page.filename = "ls-cache.txt";
+ cgit_print_http_headers();
+ cache_ls(ctx.cfg.cache_root);
+}
+
+static void objects_fn(void)
+{
+ cgit_clone_objects();
+}
+
+static void repolist_fn(void)
+{
+ cgit_print_repolist();
+}
+
+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 refs_fn(void)
+{
+ cgit_print_refs();
+}
+
+static void snapshot_fn(void)
+{
+ cgit_print_snapshot(ctx.qry.head, ctx.qry.oid, ctx.qry.path,
+ ctx.qry.nohead);
+}
+
+static void stats_fn(void)
+{
+ cgit_show_stats();
+}
+
+static void summary_fn(void)
+{
+ cgit_print_summary();
+}
+
+static void tag_fn(void)
+{
+ cgit_print_tag(ctx.qry.oid);
+}
+
+static void tree_fn(void)
+{
+ cgit_print_tree(ctx.qry.oid, ctx.qry.path);
+}
+
+#define def_cmd(name, want_repo, want_vpath, is_clone) \
+ {#name, name##_fn, want_repo, want_vpath, is_clone}
+
+struct cgit_cmd *cgit_get_cmd(void)
+{
+ static struct cgit_cmd cmds[] = {
+ def_cmd(HEAD, 1, 0, 1),
+ def_cmd(atom, 1, 0, 0),
+ def_cmd(about, 0, 0, 0),
+ def_cmd(blame, 1, 1, 0),
+ def_cmd(blob, 1, 0, 0),
+ def_cmd(commit, 1, 1, 0),
+ def_cmd(diff, 1, 1, 0),
+ def_cmd(info, 1, 0, 1),
+ def_cmd(log, 1, 1, 0),
+ def_cmd(ls_cache, 0, 0, 0),
+ def_cmd(objects, 1, 0, 1),
+ def_cmd(patch, 1, 1, 0),
+ def_cmd(plain, 1, 0, 0),
+ def_cmd(rawdiff, 1, 1, 0),
+ def_cmd(refs, 1, 0, 0),
+ def_cmd(repolist, 0, 0, 0),
+ def_cmd(snapshot, 1, 0, 0),
+ def_cmd(stats, 1, 1, 0),
+ def_cmd(summary, 1, 0, 0),
+ def_cmd(tag, 1, 0, 0),
+ def_cmd(tree, 1, 1, 0),
+ };
+ int i;
+
+ if (ctx.qry.page == NULL) {
+ if (ctx.repo)
+ ctx.qry.page = "summary";
+ else
+ ctx.qry.page = "repolist";
+ }
+
+ for (i = 0; i < sizeof(cmds)/sizeof(*cmds); i++)
+ if (!strcmp(ctx.qry.page, cmds[i].name))
+ return &cmds[i];
+ return NULL;
+}
diff --git a/source/cmd.h b/source/cmd.h
new file mode 100644
index 0000000..6249b1d
--- /dev/null
+++ b/source/cmd.h
@@ -0,0 +1,16 @@
+#ifndef CMD_H
+#define CMD_H
+
+typedef void (*cgit_cmd_fn)(void);
+
+struct cgit_cmd {
+ const char *name;
+ cgit_cmd_fn fn;
+ unsigned int want_repo:1,
+ want_vpath:1,
+ is_clone:1;
+};
+
+extern struct cgit_cmd *cgit_get_cmd(void);
+
+#endif /* CMD_H */
diff --git a/source/configfile.c b/source/configfile.c
new file mode 100644
index 0000000..e039109
--- /dev/null
+++ b/source/configfile.c
@@ -0,0 +1,90 @@
+/* configfile.c: parsing of config files
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include <git-compat-util.h>
+#include "configfile.h"
+
+static int next_char(FILE *f)
+{
+ int c = fgetc(f);
+ if (c == '\r') {
+ c = fgetc(f);
+ if (c != '\n') {
+ ungetc(c, f);
+ c = '\r';
+ }
+ }
+ return c;
+}
+
+static void skip_line(FILE *f)
+{
+ int c;
+
+ while ((c = next_char(f)) && c != '\n' && c != EOF)
+ ;
+}
+
+static int read_config_line(FILE *f, struct strbuf *name, struct strbuf *value)
+{
+ int c = next_char(f);
+
+ strbuf_reset(name);
+ strbuf_reset(value);
+
+ /* Skip comments and preceding spaces. */
+ for(;;) {
+ if (c == EOF)
+ return 0;
+ else if (c == '#' || c == ';')
+ skip_line(f);
+ else if (!isspace(c))
+ break;
+ c = next_char(f);
+ }
+
+ /* Read variable name. */
+ while (c != '=') {
+ if (c == '\n' || c == EOF)
+ return 0;
+ strbuf_addch(name, c);
+ c = next_char(f);
+ }
+
+ /* Read variable value. */
+ c = next_char(f);
+ while (c != '\n' && c != EOF) {
+ strbuf_addch(value, c);
+ c = next_char(f);
+ }
+
+ return 1;
+}
+
+int parse_configfile(const char *filename, configfile_value_fn fn)
+{
+ static int nesting;
+ struct strbuf name = STRBUF_INIT;
+ struct strbuf value = STRBUF_INIT;
+ FILE *f;
+
+ /* cancel deeply nested include-commands */
+ if (nesting > 8)
+ return -1;
+ if (!(f = fopen(filename, "r")))
+ return -1;
+ nesting++;
+ while (read_config_line(f, &name, &value))
+ fn(name.buf, value.buf);
+ nesting--;
+ fclose(f);
+ strbuf_release(&name);
+ strbuf_release(&value);
+ return 0;
+}
+
diff --git a/source/configfile.h b/source/configfile.h
new file mode 100644
index 0000000..af7ca19
--- /dev/null
+++ b/source/configfile.h
@@ -0,0 +1,10 @@
+#ifndef CONFIGFILE_H
+#define CONFIGFILE_H
+
+#include "cgit.h"
+
+typedef void (*configfile_value_fn)(const char *name, const char *value);
+
+extern int parse_configfile(const char *filename, configfile_value_fn fn);
+
+#endif /* CONFIGFILE_H */
diff --git a/source/filter.c b/source/filter.c
new file mode 100644
index 0000000..c778d05
--- /dev/null
+++ b/source/filter.c
@@ -0,0 +1,457 @@
+/* filter.c: filter framework functions
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "html.h"
+#ifndef NO_LUA
+#include <dlfcn.h>
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+#endif
+
+static inline void reap_filter(struct cgit_filter *filter)
+{
+ if (filter && filter->cleanup)
+ filter->cleanup(filter);
+}
+
+void cgit_cleanup_filters(void)
+{
+ int i;
+ reap_filter(ctx.cfg.about_filter);
+ reap_filter(ctx.cfg.commit_filter);
+ reap_filter(ctx.cfg.source_filter);
+ reap_filter(ctx.cfg.email_filter);
+ reap_filter(ctx.cfg.owner_filter);
+ reap_filter(ctx.cfg.auth_filter);
+ for (i = 0; i < cgit_repolist.count; ++i) {
+ reap_filter(cgit_repolist.repos[i].about_filter);
+ reap_filter(cgit_repolist.repos[i].commit_filter);
+ reap_filter(cgit_repolist.repos[i].source_filter);
+ reap_filter(cgit_repolist.repos[i].email_filter);
+ reap_filter(cgit_repolist.repos[i].owner_filter);
+ }
+}
+
+static int open_exec_filter(struct cgit_filter *base, va_list ap)
+{
+ struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+ int pipe_fh[2];
+ int i;
+
+ for (i = 0; i < filter->base.argument_count; i++)
+ filter->argv[i + 1] = va_arg(ap, char *);
+
+ filter->old_stdout = chk_positive(dup(STDOUT_FILENO),
+ "Unable to duplicate STDOUT");
+ chk_zero(pipe(pipe_fh), "Unable to create pipe to subprocess");
+ filter->pid = chk_non_negative(fork(), "Unable to create subprocess");
+ if (filter->pid == 0) {
+ close(pipe_fh[1]);
+ chk_non_negative(dup2(pipe_fh[0], STDIN_FILENO),
+ "Unable to use pipe as STDIN");
+ execvp(filter->cmd, filter->argv);
+ die_errno("Unable to exec subprocess %s", filter->cmd);
+ }
+ close(pipe_fh[0]);
+ chk_non_negative(dup2(pipe_fh[1], STDOUT_FILENO),
+ "Unable to use pipe as STDOUT");
+ close(pipe_fh[1]);
+ return 0;
+}
+
+static int close_exec_filter(struct cgit_filter *base)
+{
+ struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+ int i, exit_status = 0;
+
+ chk_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))
+ goto done;
+ die("Subprocess %s exited abnormally", filter->cmd);
+
+done:
+ for (i = 0; i < filter->base.argument_count; i++)
+ filter->argv[i + 1] = NULL;
+ return WEXITSTATUS(exit_status);
+
+}
+
+static void fprintf_exec_filter(struct cgit_filter *base, FILE *f, const char *prefix)
+{
+ struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+ fprintf(f, "%sexec:%s\n", prefix, filter->cmd);
+}
+
+static void cleanup_exec_filter(struct cgit_filter *base)
+{
+ struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
+ if (filter->argv) {
+ free(filter->argv);
+ filter->argv = NULL;
+ }
+ if (filter->cmd) {
+ free(filter->cmd);
+ filter->cmd = NULL;
+ }
+}
+
+static struct cgit_filter *new_exec_filter(const char *cmd, int argument_count)
+{
+ struct cgit_exec_filter *f;
+ int args_size = 0;
+
+ f = xmalloc(sizeof(*f));
+ /* We leave argv for now and assign it below. */
+ cgit_exec_filter_init(f, strdup_first_line(cmd), NULL);
+ f->base.argument_count = argument_count;
+ args_size = (2 + argument_count) * sizeof(char *);
+ f->argv = xmalloc(args_size);
+ memset(f->argv, 0, args_size);
+ f->argv[0] = f->cmd;
+ return &f->base;
+}
+
+void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv)
+{
+ memset(filter, 0, sizeof(*filter));
+ filter->base.open = open_exec_filter;
+ filter->base.close = close_exec_filter;
+ filter->base.fprintfp = fprintf_exec_filter;
+ filter->base.cleanup = cleanup_exec_filter;
+ filter->cmd = cmd;
+ filter->argv = argv;
+ /* The argument count for open_filter is zero by default, unless called from new_filter, above. */
+ filter->base.argument_count = 0;
+}
+
+#ifdef NO_LUA
+void cgit_init_filters(void)
+{
+}
+#endif
+
+#ifndef NO_LUA
+static ssize_t (*libc_write)(int fd, const void *buf, size_t count);
+static ssize_t (*filter_write)(struct cgit_filter *base, const void *buf, size_t count) = NULL;
+static struct cgit_filter *current_write_filter = NULL;
+
+void cgit_init_filters(void)
+{
+ libc_write = dlsym(RTLD_NEXT, "write");
+ if (!libc_write)
+ die("Could not locate libc's write function");
+}
+
+ssize_t write(int fd, const void *buf, size_t count)
+{
+ if (fd != STDOUT_FILENO || !filter_write)
+ return libc_write(fd, buf, count);
+ return filter_write(current_write_filter, buf, count);
+}
+
+static inline void hook_write(struct cgit_filter *filter, ssize_t (*new_write)(struct cgit_filter *base, const void *buf, size_t count))
+{
+ /* We want to avoid buggy nested patterns. */
+ assert(filter_write == NULL);
+ assert(current_write_filter == NULL);
+ current_write_filter = filter;
+ filter_write = new_write;
+}
+
+static inline void unhook_write(void)
+{
+ assert(filter_write != NULL);
+ assert(current_write_filter != NULL);
+ filter_write = NULL;
+ current_write_filter = NULL;
+}
+
+struct lua_filter {
+ struct cgit_filter base;
+ char *script_file;
+ lua_State *lua_state;
+};
+
+static void error_lua_filter(struct lua_filter *filter)
+{
+ die("Lua error in %s: %s", filter->script_file, lua_tostring(filter->lua_state, -1));
+ lua_pop(filter->lua_state, 1);
+}
+
+static ssize_t write_lua_filter(struct cgit_filter *base, const void *buf, size_t count)
+{
+ struct lua_filter *filter = (struct lua_filter *)base;
+
+ lua_getglobal(filter->lua_state, "filter_write");
+ lua_pushlstring(filter->lua_state, buf, count);
+ if (lua_pcall(filter->lua_state, 1, 0, 0)) {
+ error_lua_filter(filter);
+ errno = EIO;
+ return -1;
+ }
+ return count;
+}
+
+static inline int hook_lua_filter(lua_State *lua_state, void (*fn)(const char *txt))
+{
+ const char *str;
+ ssize_t (*save_filter_write)(struct cgit_filter *base, const void *buf, size_t count);
+ struct cgit_filter *save_filter;
+
+ str = lua_tostring(lua_state, 1);
+ if (!str)
+ return 0;
+
+ save_filter_write = filter_write;
+ save_filter = current_write_filter;
+ unhook_write();
+ fn(str);
+ hook_write(save_filter, save_filter_write);
+
+ return 0;
+}
+
+static int html_lua_filter(lua_State *lua_state)
+{
+ return hook_lua_filter(lua_state, html);
+}
+
+static int html_txt_lua_filter(lua_State *lua_state)
+{
+ return hook_lua_filter(lua_state, html_txt);
+}
+
+static int html_attr_lua_filter(lua_State *lua_state)
+{
+ return hook_lua_filter(lua_state, html_attr);
+}
+
+static int html_url_path_lua_filter(lua_State *lua_state)
+{
+ return hook_lua_filter(lua_state, html_url_path);
+}
+
+static int html_url_arg_lua_filter(lua_State *lua_state)
+{
+ return hook_lua_filter(lua_state, html_url_arg);
+}
+
+static int html_include_lua_filter(lua_State *lua_state)
+{
+ return hook_lua_filter(lua_state, (void (*)(const char *))html_include);
+}
+
+static void cleanup_lua_filter(struct cgit_filter *base)
+{
+ struct lua_filter *filter = (struct lua_filter *)base;
+
+ if (!filter->lua_state)
+ return;
+
+ lua_close(filter->lua_state);
+ filter->lua_state = NULL;
+ if (filter->script_file) {
+ free(filter->script_file);
+ filter->script_file = NULL;
+ }
+}
+
+static int init_lua_filter(struct lua_filter *filter)
+{
+ if (filter->lua_state)
+ return 0;
+
+ if (!(filter->lua_state = luaL_newstate()))
+ return 1;
+
+ luaL_openlibs(filter->lua_state);
+
+ lua_pushcfunction(filter->lua_state, html_lua_filter);
+ lua_setglobal(filter->lua_state, "html");
+ lua_pushcfunction(filter->lua_state, html_txt_lua_filter);
+ lua_setglobal(filter->lua_state, "html_txt");
+ lua_pushcfunction(filter->lua_state, html_attr_lua_filter);
+ lua_setglobal(filter->lua_state, "html_attr");
+ lua_pushcfunction(filter->lua_state, html_url_path_lua_filter);
+ lua_setglobal(filter->lua_state, "html_url_path");
+ lua_pushcfunction(filter->lua_state, html_url_arg_lua_filter);
+ lua_setglobal(filter->lua_state, "html_url_arg");
+ lua_pushcfunction(filter->lua_state, html_include_lua_filter);
+ lua_setglobal(filter->lua_state, "html_include");
+
+ if (luaL_dofile(filter->lua_state, filter->script_file)) {
+ error_lua_filter(filter);
+ lua_close(filter->lua_state);
+ filter->lua_state = NULL;
+ return 1;
+ }
+ return 0;
+}
+
+static int open_lua_filter(struct cgit_filter *base, va_list ap)
+{
+ struct lua_filter *filter = (struct lua_filter *)base;
+ int i;
+
+ if (init_lua_filter(filter))
+ return 1;
+
+ hook_write(base, write_lua_filter);
+
+ lua_getglobal(filter->lua_state, "filter_open");
+ for (i = 0; i < filter->base.argument_count; ++i)
+ lua_pushstring(filter->lua_state, va_arg(ap, char *));
+ if (lua_pcall(filter->lua_state, filter->base.argument_count, 0, 0)) {
+ error_lua_filter(filter);
+ return 1;
+ }
+ return 0;
+}
+
+static int close_lua_filter(struct cgit_filter *base)
+{
+ struct lua_filter *filter = (struct lua_filter *)base;
+ int ret = 0;
+
+ lua_getglobal(filter->lua_state, "filter_close");
+ if (lua_pcall(filter->lua_state, 0, 1, 0)) {
+ error_lua_filter(filter);
+ ret = -1;
+ } else {
+ ret = lua_tonumber(filter->lua_state, -1);
+ lua_pop(filter->lua_state, 1);
+ }
+
+ unhook_write();
+ return ret;
+}
+
+static void fprintf_lua_filter(struct cgit_filter *base, FILE *f, const char *prefix)
+{
+ struct lua_filter *filter = (struct lua_filter *)base;
+ fprintf(f, "%slua:%s\n", prefix, filter->script_file);
+}
+
+
+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->base.open = open_lua_filter;
+ filter->base.close = close_lua_filter;
+ filter->base.fprintfp = fprintf_lua_filter;
+ filter->base.cleanup = cleanup_lua_filter;
+ filter->base.argument_count = argument_count;
+ filter->script_file = strdup_first_line(cmd);
+
+ return &filter->base;
+}
+
+#endif
+
+
+int cgit_open_filter(struct cgit_filter *filter, ...)
+{
+ int result;
+ va_list ap;
+ if (!filter)
+ return 0;
+ va_start(ap, filter);
+ result = filter->open(filter, ap);
+ va_end(ap);
+ return result;
+}
+
+int cgit_close_filter(struct cgit_filter *filter)
+{
+ if (!filter)
+ return 0;
+ return filter->close(filter);
+}
+
+void cgit_fprintf_filter(struct cgit_filter *filter, FILE *f, const char *prefix)
+{
+ filter->fprintfp(filter, f, prefix);
+}
+
+
+
+static const struct {
+ const char *prefix;
+ struct cgit_filter *(*ctor)(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;
+ int i;
+ size_t len;
+ int argument_count;
+
+ if (!cmd || !cmd[0])
+ return NULL;
+
+ colon = strchr(cmd, ':');
+ len = colon - cmd;
+ /*
+ * In case we're running on Windows, don't allow a single letter before
+ * the colon.
+ */
+ if (len == 1)
+ colon = NULL;
+
+ switch (filtertype) {
+ case AUTH:
+ argument_count = 12;
+ break;
+
+ case EMAIL:
+ argument_count = 2;
+ break;
+
+ case OWNER:
+ argument_count = 0;
+ break;
+
+ case SOURCE:
+ case ABOUT:
+ argument_count = 1;
+ break;
+
+ case COMMIT:
+ default:
+ argument_count = 0;
+ break;
+ }
+
+ /* If no prefix is given, exec filter is the default. */
+ 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].ctor(colon + 1, argument_count);
+ }
+
+ die("Invalid filter type: %.*s", (int) len, cmd);
+}
diff --git a/source/gen-version.sh b/source/gen-version.sh
new file mode 100755
index 0000000..23d0d0b
--- /dev/null
+++ b/source/gen-version.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+# Writes a "CGIT_VERSION = ..." line to the output file (default: VERSION),
+# for the build to include. Run from the project root so `git describe` acts
+# on cgit rather than the bundled Git submodule.
+#
+# Usage: gen-version.sh <fallback-version> [output-file]
+
+V=$1
+OUT=${2:-VERSION}
+
+# Prefer `git describe` when run at the top of the cgit working tree.
+if test "$(git rev-parse --git-dir 2>/dev/null)" = '.git'
+then
+ V=$(git describe --abbrev=4 HEAD 2>/dev/null)
+fi
+
+new="CGIT_VERSION = $V"
+old=$(cat "$OUT" 2>/dev/null)
+
+# Exit if the version is already up to date.
+test "$old" = "$new" && exit 0
+
+echo "$new" > "$OUT"
+cat "$OUT"
diff --git a/source/html.c b/source/html.c
new file mode 100644
index 0000000..0bac34b
--- /dev/null
+++ b/source/html.c
@@ -0,0 +1,344 @@
+/* html.c: helper functions for html output
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "html.h"
+#include "url.h"
+
+/* Percent-encoding of each character, except: a-zA-Z0-9!$()*,./:;@- */
+static const char* url_escape_table[256] = {
+ "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
+ "%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",
+ "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
+ "%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f",
+ "%20", NULL, "%22", "%23", NULL, "%25", "%26", "%27",
+ NULL, NULL, NULL, "%2b", NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, "%3c", "%3d", "%3e", "%3f",
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, "%5c", NULL, "%5e", NULL,
+ "%60", NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, "%7b", "%7c", "%7d", NULL, "%7f",
+ "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
+ "%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f",
+ "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
+ "%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f",
+ "%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7",
+ "%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af",
+ "%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7",
+ "%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf",
+ "%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7",
+ "%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf",
+ "%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7",
+ "%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df",
+ "%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7",
+ "%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef",
+ "%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7",
+ "%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff"
+};
+
+char *fmt(const char *format, ...)
+{
+ static char buf[8][1024];
+ static int bufidx;
+ int len;
+ va_list args;
+
+ bufidx++;
+ bufidx &= 7;
+
+ va_start(args, format);
+ len = vsnprintf(buf[bufidx], sizeof(buf[bufidx]), format, args);
+ va_end(args);
+ if (len >= sizeof(buf[bufidx])) {
+ fprintf(stderr, "[html.c] string truncated: %s\n", format);
+ exit(1);
+ }
+ return buf[bufidx];
+}
+
+char *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_raw(const char *data, size_t size)
+{
+ if (write(STDOUT_FILENO, data, size) != size)
+ die_errno("write error on html output");
+}
+
+void html(const char *txt)
+{
+ html_raw(txt, strlen(txt));
+}
+
+void htmlf(const char *format, ...)
+{
+ va_list args;
+ struct strbuf buf = STRBUF_INIT;
+
+ va_start(args, format);
+ strbuf_vaddf(&buf, format, args);
+ va_end(args);
+ html(buf.buf);
+ strbuf_release(&buf);
+}
+
+void html_txtf(const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ html_vtxtf(format, args);
+ va_end(args);
+}
+
+void html_vtxtf(const char *format, va_list ap)
+{
+ va_list cp;
+ struct strbuf buf = STRBUF_INIT;
+
+ va_copy(cp, ap);
+ strbuf_vaddf(&buf, format, cp);
+ va_end(cp);
+ html_txt(buf.buf);
+ strbuf_release(&buf);
+}
+
+void html_txt(const char *txt)
+{
+ if (txt)
+ html_ntxt(txt, strlen(txt));
+}
+
+ssize_t html_ntxt(const char *txt, size_t len)
+{
+ const char *t = txt;
+ ssize_t slen;
+
+ if (len > SSIZE_MAX)
+ return -1;
+
+ slen = (ssize_t) len;
+ while (t && *t && slen--) {
+ int c = *t;
+ if (c == '<' || c == '>' || c == '&') {
+ html_raw(txt, t - txt);
+ if (c == '>')
+ html("&gt;");
+ else if (c == '<')
+ html("&lt;");
+ else if (c == '&')
+ html("&amp;");
+ txt = t + 1;
+ }
+ t++;
+ }
+ if (t != txt)
+ html_raw(txt, t - txt);
+ return slen;
+}
+
+void html_attrf(const char *fmt, ...)
+{
+ va_list ap;
+ struct strbuf sb = STRBUF_INIT;
+
+ va_start(ap, fmt);
+ strbuf_vaddf(&sb, fmt, ap);
+ va_end(ap);
+
+ html_attr(sb.buf);
+ strbuf_release(&sb);
+}
+
+void html_attr(const char *txt)
+{
+ const char *t = txt;
+ while (t && *t) {
+ int c = *t;
+ if (c == '<' || c == '>' || c == '\'' || c == '\"' || c == '&') {
+ html_raw(txt, t - 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;");
+ txt = t + 1;
+ }
+ t++;
+ }
+ if (t != txt)
+ html(txt);
+}
+
+void html_url_path(const char *txt)
+{
+ const char *t = txt;
+ while (t && *t) {
+ unsigned char c = *t;
+ const char *e = url_escape_table[c];
+ if (e && c != '+' && c != '&') {
+ html_raw(txt, t - txt);
+ html(e);
+ txt = t + 1;
+ }
+ t++;
+ }
+ if (t != txt)
+ html(txt);
+}
+
+void html_url_arg(const char *txt)
+{
+ const char *t = txt;
+ while (t && *t) {
+ unsigned char c = *t;
+ const char *e = url_escape_table[c];
+ if (c == ' ')
+ e = "+";
+ if (e) {
+ html_raw(txt, t - txt);
+ html(e);
+ txt = t + 1;
+ }
+ t++;
+ }
+ if (t != txt)
+ html(txt);
+}
+
+void html_header_arg_in_quotes(const char *txt)
+{
+ const char *t = txt;
+ while (t && *t) {
+ unsigned char c = *t;
+ const char *e = NULL;
+ if (c == '\\')
+ e = "\\\\";
+ else if (c == '\r')
+ e = "\\r";
+ else if (c == '\n')
+ e = "\\n";
+ else if (c == '"')
+ e = "\\\"";
+ if (e) {
+ html_raw(txt, t - txt);
+ html(e);
+ txt = t + 1;
+ }
+ t++;
+ }
+ if (t != txt)
+ html(txt);
+
+}
+
+void html_hidden(const char *name, const char *value)
+{
+ html("<input type='hidden' name='");
+ html_attr(name);
+ html("' value='");
+ html_attr(value);
+ html("'/>");
+}
+
+void html_option(const char *value, const char *text, const char *selected_value)
+{
+ html("<option value='");
+ html_attr(value);
+ html("'");
+ if (selected_value && !strcmp(selected_value, value))
+ html(" selected='selected'");
+ html(">");
+ html_txt(text);
+ html("</option>\n");
+}
+
+void html_intoption(int value, const char *text, int selected_value)
+{
+ htmlf("<option value='%d'%s>", value,
+ value == selected_value ? " selected='selected'" : "");
+ html_txt(text);
+ html("</option>");
+}
+
+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>");
+}
+
+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)
+{
+ FILE *f;
+ char buf[4096];
+ size_t len;
+
+ if (!(f = fopen(filename, "r"))) {
+ fprintf(stderr, "[cgit] Failed to include file %s: %s (%d).\n",
+ filename, strerror(errno), errno);
+ return -1;
+ }
+ while ((len = fread(buf, 1, 4096, 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 *t = txt;
+
+ while (t && *t) {
+ char *name = url_decode_parameter_name(&t);
+ if (*name) {
+ char *value = url_decode_parameter_value(&t);
+ fn(name, value);
+ free(value);
+ }
+ free(name);
+ }
+}
diff --git a/source/html.h b/source/html.h
new file mode 100644
index 0000000..fa4de77
--- /dev/null
+++ b/source/html.h
@@ -0,0 +1,37 @@
+#ifndef HTML_H
+#define HTML_H
+
+#include "cgit.h"
+
+extern void html_raw(const char *txt, size_t size);
+extern void html(const char *txt);
+
+__attribute__((format (printf,1,2)))
+extern void htmlf(const char *format,...);
+
+__attribute__((format (printf,1,2)))
+extern void html_txtf(const char *format,...);
+
+__attribute__((format (printf,1,0)))
+extern void html_vtxtf(const char *format, va_list ap);
+
+__attribute__((format (printf,1,2)))
+extern void html_attrf(const char *format,...);
+
+extern void html_txt(const char *txt);
+extern ssize_t html_ntxt(const char *txt, size_t len);
+extern void html_attr(const char *txt);
+extern void html_url_path(const char *txt);
+extern void html_url_arg(const char *txt);
+extern void html_header_arg_in_quotes(const char *txt);
+extern void html_hidden(const char *name, const char *value);
+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));
+
+#endif /* HTML_H */
diff --git a/source/parsing.c b/source/parsing.c
new file mode 100644
index 0000000..bdaed25
--- /dev/null
+++ b/source/parsing.c
@@ -0,0 +1,227 @@
+/* parsing.c: parsing of config files
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+
+/*
+ * url syntax: [repo ['/' cmd [ '/' path]]]
+ * repo: any valid repo url, may contain '/'
+ * cmd: log | commit | diff | tree | view | blob | snapshot
+ * path: any valid path, may contain '/'
+ *
+ */
+void cgit_parse_url(const char *url)
+{
+ char *c, *cmd, *p, *buf;
+ struct cgit_repo *repo;
+
+ if (!url || url[0] == '\0')
+ return;
+
+ ctx.qry.page = NULL;
+ ctx.repo = cgit_get_repoinfo(url);
+ if (ctx.repo) {
+ ctx.qry.repo = ctx.repo->url;
+ return;
+ }
+
+ buf = xstrdup(url);
+ cmd = NULL;
+ c = strchr(buf, '/');
+ while (c) {
+ c[0] = '\0';
+ repo = cgit_get_repoinfo(buf);
+ if (repo) {
+ ctx.repo = repo;
+ cmd = c;
+ }
+ c[0] = '/';
+ c = strchr(c + 1, '/');
+ }
+
+ if (ctx.repo) {
+ ctx.qry.repo = ctx.repo->url;
+ p = strchr(cmd + 1, '/');
+ if (p) {
+ p[0] = '\0';
+ if (p[1])
+ ctx.qry.path = trim_end(p + 1, '/');
+ }
+ if (cmd[1])
+ ctx.qry.page = xstrdup(cmd + 1);
+ }
+ free(buf);
+}
+
+static char *substr(const char *head, const char *tail)
+{
+ char *buf;
+
+ if (tail < head)
+ return xstrdup("");
+ buf = xmalloc(tail - head + 1);
+ strlcpy(buf, head, tail - head + 1);
+ return buf;
+}
+
+static void parse_user(const char *t, char **name, char **email, unsigned long *date, int *tz)
+{
+ struct ident_split ident;
+ unsigned email_len;
+
+ if (!split_ident_line(&ident, t, strchrnul(t, '\n') - t)) {
+ *name = substr(ident.name_begin, ident.name_end);
+
+ email_len = ident.mail_end - ident.mail_begin;
+ *email = xmalloc(strlen("<") + email_len + strlen(">") + 1);
+ xsnprintf(*email, email_len + 3, "<%.*s>", email_len, ident.mail_begin);
+
+ if (ident.date_begin)
+ *date = strtoul(ident.date_begin, NULL, 10);
+ if (ident.tz_begin)
+ *tz = atoi(ident.tz_begin);
+ }
+}
+
+#ifdef NO_ICONV
+#define reencode(a, b, c)
+#else
+static const char *reencode(char **txt, const char *src_enc, const char *dst_enc)
+{
+ char *tmp;
+
+ if (!txt)
+ return NULL;
+
+ if (!*txt || !src_enc || !dst_enc)
+ return *txt;
+
+ /* no encoding needed if src_enc equals dst_enc */
+ if (!strcasecmp(src_enc, dst_enc))
+ return *txt;
+
+ tmp = reencode_string(*txt, dst_enc, src_enc);
+ if (tmp) {
+ free(*txt);
+ *txt = tmp;
+ }
+ return *txt;
+}
+#endif
+
+static const char *next_header_line(const char *p)
+{
+ p = strchr(p, '\n');
+ if (!p)
+ return NULL;
+ return p + 1;
+}
+
+static int end_of_header(const char *p)
+{
+ return !p || (*p == '\n');
+}
+
+struct commitinfo *cgit_parse_commit(struct commit *commit)
+{
+ struct commitinfo *ret;
+ const char *p = repo_get_commit_buffer(the_repository, commit, NULL);
+ const char *t;
+
+ ret = xcalloc(1, sizeof(struct commitinfo));
+ ret->commit = commit;
+
+ if (!p)
+ return ret;
+
+ if (!skip_prefix(p, "tree ", &p))
+ die("Bad commit: %s", oid_to_hex(&commit->object.oid));
+ p += the_hash_algo->hexsz + 1;
+
+ while (skip_prefix(p, "parent ", &p))
+ p += the_hash_algo->hexsz + 1;
+
+ if (p && skip_prefix(p, "author ", &p)) {
+ parse_user(p, &ret->author, &ret->author_email,
+ &ret->author_date, &ret->author_tz);
+ p = next_header_line(p);
+ }
+
+ if (p && skip_prefix(p, "committer ", &p)) {
+ parse_user(p, &ret->committer, &ret->committer_email,
+ &ret->committer_date, &ret->committer_tz);
+ p = next_header_line(p);
+ }
+
+ if (p && skip_prefix(p, "encoding ", &p)) {
+ t = strchr(p, '\n');
+ if (t) {
+ ret->msg_encoding = substr(p, t + 1);
+ p = t + 1;
+ }
+ }
+
+ if (!ret->msg_encoding)
+ ret->msg_encoding = xstrdup("UTF-8");
+
+ while (!end_of_header(p))
+ p = next_header_line(p);
+ while (p && *p == '\n')
+ p++;
+ if (!p)
+ return ret;
+
+ t = strchrnul(p, '\n');
+ ret->subject = substr(p, t);
+ while (*t == '\n')
+ t++;
+ ret->msg = xstrdup(t);
+
+ reencode(&ret->author, ret->msg_encoding, PAGE_ENCODING);
+ reencode(&ret->author_email, ret->msg_encoding, PAGE_ENCODING);
+ reencode(&ret->committer, ret->msg_encoding, PAGE_ENCODING);
+ reencode(&ret->committer_email, ret->msg_encoding, PAGE_ENCODING);
+ reencode(&ret->subject, ret->msg_encoding, PAGE_ENCODING);
+ reencode(&ret->msg, ret->msg_encoding, PAGE_ENCODING);
+
+ return ret;
+}
+
+struct taginfo *cgit_parse_tag(struct tag *tag)
+{
+ void *data;
+ enum object_type type;
+ unsigned long size;
+ const char *p;
+ struct taginfo *ret = NULL;
+
+ data = odb_read_object(the_repository->objects, &tag->object.oid, &type, &size);
+ if (!data || type != OBJ_TAG)
+ goto cleanup;
+
+ ret = xcalloc(1, sizeof(struct taginfo));
+
+ for (p = data; !end_of_header(p); p = next_header_line(p)) {
+ if (skip_prefix(p, "tagger ", &p)) {
+ parse_user(p, &ret->tagger, &ret->tagger_email,
+ &ret->tagger_date, &ret->tagger_tz);
+ }
+ }
+
+ while (p && *p == '\n')
+ p++;
+
+ if (p && *p)
+ ret->msg = xstrdup(p);
+
+cleanup:
+ free(data);
+ return ret;
+}
diff --git a/source/scan-tree.c b/source/scan-tree.c
new file mode 100644
index 0000000..c120efe
--- /dev/null
+++ b/source/scan-tree.c
@@ -0,0 +1,269 @@
+/* scan-tree.c
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "scan-tree.h"
+#include "configfile.h"
+#include "html.h"
+#include <config.h>
+
+/* return 1 if path contains a objects/ directory and a HEAD file */
+static int is_git_dir(const char *path)
+{
+ struct stat st;
+ struct strbuf pathbuf = STRBUF_INIT;
+ int result = 0;
+
+ strbuf_addf(&pathbuf, "%s/objects", path);
+ if (stat(pathbuf.buf, &st)) {
+ if (errno != ENOENT)
+ fprintf(stderr, "Error checking path %s: %s (%d)\n",
+ path, strerror(errno), errno);
+ goto out;
+ }
+ if (!S_ISDIR(st.st_mode))
+ goto out;
+
+ strbuf_reset(&pathbuf);
+ strbuf_addf(&pathbuf, "%s/HEAD", path);
+ if (stat(pathbuf.buf, &st)) {
+ if (errno != ENOENT)
+ fprintf(stderr, "Error checking path %s: %s (%d)\n",
+ path, strerror(errno), errno);
+ goto out;
+ }
+ if (!S_ISREG(st.st_mode))
+ goto out;
+
+ result = 1;
+out:
+ strbuf_release(&pathbuf);
+ return result;
+}
+
+static struct cgit_repo *repo;
+
+static void scan_tree_repo_config(const char *name, const char *value)
+{
+ cgit_repo_config(repo, name, value);
+}
+
+static int gitconfig_config(const char *key, const char *value,
+ const __attribute__((unused)) struct config_context *ctx, void *cb)
+{
+ const char *name;
+
+ if (!strcmp(key, "gitweb.owner"))
+ cgit_repo_config(repo, "owner", value);
+ else if (!strcmp(key, "gitweb.description"))
+ cgit_repo_config(repo, "desc", value);
+ else if (!strcmp(key, "gitweb.category"))
+ cgit_repo_config(repo, "section", value);
+ else if (!strcmp(key, "gitweb.homepage"))
+ cgit_repo_config(repo, "homepage", value);
+ else if (skip_prefix(key, "cgit.", &name))
+ cgit_repo_config(repo, name, value);
+
+ return 0;
+}
+
+static char *xstrrchr(char *s, char *from, int c)
+{
+ while (from >= s && *from != c)
+ from--;
+ return from < s ? NULL : from;
+}
+
+static void add_repo(const char *base, struct strbuf *path)
+{
+ struct stat st;
+ struct passwd *pwd;
+ size_t pathlen;
+ struct strbuf rel = STRBUF_INIT;
+ char *p, *slash;
+ int n;
+ size_t size;
+
+ if (stat(path->buf, &st)) {
+ fprintf(stderr, "Error accessing %s: %s (%d)\n",
+ path->buf, strerror(errno), errno);
+ return;
+ }
+
+ strbuf_addch(path, '/');
+ pathlen = path->len;
+
+ if (ctx.cfg.strict_export) {
+ strbuf_addstr(path, ctx.cfg.strict_export);
+ if(stat(path->buf, &st))
+ return;
+ strbuf_setlen(path, pathlen);
+ }
+
+ strbuf_addstr(path, "noweb");
+ if (!stat(path->buf, &st))
+ return;
+ strbuf_setlen(path, pathlen);
+
+ if (!starts_with(path->buf, base))
+ strbuf_addbuf(&rel, path);
+ else
+ strbuf_addstr(&rel, path->buf + strlen(base) + 1);
+
+ if (!strcmp(rel.buf + rel.len - 5, "/.git"))
+ strbuf_setlen(&rel, rel.len - 5);
+ else if (rel.len && rel.buf[rel.len - 1] == '/')
+ strbuf_setlen(&rel, rel.len - 1);
+
+ repo = cgit_add_repo(rel.buf);
+ if (ctx.cfg.enable_git_config) {
+ strbuf_addstr(path, "config");
+ git_config_from_file(gitconfig_config, path->buf, NULL);
+ strbuf_setlen(path, pathlen);
+ }
+
+ if (ctx.cfg.remove_suffix) {
+ size_t urllen;
+ strip_suffix(repo->url, ".git", &urllen);
+ strip_suffix_mem(repo->url, &urllen, "/");
+ repo->url[urllen] = '\0';
+ }
+ repo->path = strdup_first_line(path->buf);
+ while (!repo->owner) {
+ if ((pwd = getpwuid(st.st_uid)) == NULL) {
+ fprintf(stderr, "Error reading owner-info for %s: %s (%d)\n",
+ path->buf, strerror(errno), errno);
+ break;
+ }
+ if (pwd->pw_gecos)
+ if ((p = strchr(pwd->pw_gecos, ',')))
+ *p = '\0';
+ repo->owner = strdup_first_line(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name);
+ }
+
+ if (repo->desc == cgit_default_repo_desc || !repo->desc) {
+ strbuf_addstr(path, "description");
+ if (!stat(path->buf, &st))
+ read_first_line(path->buf, &repo->desc, &size);
+ strbuf_setlen(path, pathlen);
+ }
+
+ if (ctx.cfg.section_from_path) {
+ n = ctx.cfg.section_from_path;
+ if (n > 0) {
+ slash = rel.buf - 1;
+ while (slash && n && (slash = strchr(slash + 1, '/')))
+ n--;
+ } else {
+ slash = rel.buf + rel.len;
+ while (slash && n && (slash = xstrrchr(rel.buf, slash - 1, '/')))
+ n++;
+ }
+ if (slash && !n) {
+ *slash = '\0';
+ repo->section = strdup_first_line(rel.buf);
+ *slash = '/';
+ if (starts_with(repo->name, repo->section)) {
+ repo->name += strlen(repo->section);
+ if (*repo->name == '/')
+ repo->name++;
+ }
+ }
+ }
+
+ strbuf_addstr(path, "cgitrc");
+ if (!stat(path->buf, &st))
+ parse_configfile(path->buf, &scan_tree_repo_config);
+
+ strbuf_release(&rel);
+}
+
+static void scan_path(const char *base, const char *path)
+{
+ DIR *dir = opendir(path);
+ struct dirent *ent;
+ struct strbuf pathbuf = STRBUF_INIT;
+ size_t pathlen = strlen(path);
+ struct stat st;
+
+ if (!dir) {
+ fprintf(stderr, "Error opening directory %s: %s (%d)\n",
+ path, strerror(errno), errno);
+ return;
+ }
+
+ strbuf_add(&pathbuf, path, strlen(path));
+ if (is_git_dir(pathbuf.buf)) {
+ add_repo(base, &pathbuf);
+ goto end;
+ }
+ strbuf_addstr(&pathbuf, "/.git");
+ if (is_git_dir(pathbuf.buf)) {
+ add_repo(base, &pathbuf);
+ goto end;
+ }
+ /*
+ * Add one because we don't want to lose the trailing '/' when we
+ * reset the length of pathbuf in the loop below.
+ */
+ pathlen++;
+ while ((ent = readdir(dir)) != NULL) {
+ if (ent->d_name[0] == '.') {
+ if (ent->d_name[1] == '\0')
+ continue;
+ if (ent->d_name[1] == '.' && ent->d_name[2] == '\0')
+ continue;
+ if (!ctx.cfg.scan_hidden_path)
+ continue;
+ }
+ strbuf_setlen(&pathbuf, pathlen);
+ strbuf_addstr(&pathbuf, ent->d_name);
+ if (stat(pathbuf.buf, &st)) {
+ fprintf(stderr, "Error checking path %s: %s (%d)\n",
+ pathbuf.buf, strerror(errno), errno);
+ continue;
+ }
+ if (S_ISDIR(st.st_mode))
+ scan_path(base, pathbuf.buf);
+ }
+end:
+ strbuf_release(&pathbuf);
+ closedir(dir);
+}
+
+void scan_projects(const char *path, const char *projectsfile)
+{
+ struct strbuf line = STRBUF_INIT;
+ FILE *projects;
+ int err;
+
+ projects = fopen(projectsfile, "r");
+ if (!projects) {
+ fprintf(stderr, "Error opening projectsfile %s: %s (%d)\n",
+ projectsfile, strerror(errno), errno);
+ return;
+ }
+ while (strbuf_getline(&line, projects) != EOF) {
+ if (!line.len)
+ continue;
+ strbuf_insert(&line, 0, "/", 1);
+ strbuf_insert(&line, 0, path, strlen(path));
+ scan_path(path, line.buf);
+ }
+ if ((err = ferror(projects))) {
+ fprintf(stderr, "Error reading from projectsfile %s: %s (%d)\n",
+ projectsfile, strerror(err), err);
+ }
+ fclose(projects);
+ strbuf_release(&line);
+}
+
+void scan_tree(const char *path)
+{
+ scan_path(path, path);
+}
diff --git a/source/scan-tree.h b/source/scan-tree.h
new file mode 100644
index 0000000..def0a7a
--- /dev/null
+++ b/source/scan-tree.h
@@ -0,0 +1,2 @@
+extern void scan_projects(const char *path, const char *projectsfile);
+extern void scan_tree(const char *path);
diff --git a/source/shared.c b/source/shared.c
new file mode 100644
index 0000000..a39394d
--- /dev/null
+++ b/source/shared.c
This diff is too large to be rendered inline. View it on its own page.
diff --git a/source/ui-atom.c b/source/ui-atom.c
new file mode 100644
index 0000000..0659e96
--- /dev/null
+++ b/source/ui-atom.c
@@ -0,0 +1,159 @@
+/* ui-atom.c: functions for atom feeds
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-atom.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static void add_entry(struct commit *commit, const char *host)
+{
+ char delim = '&';
+ char *hex;
+ char *mail, *t, *t2;
+ struct commitinfo *info;
+
+ info = cgit_parse_commit(commit);
+ hex = oid_to_hex(&commit->object.oid);
+ html("<entry>\n");
+ html("<title>");
+ html_txt(info->subject);
+ html("</title>\n");
+ html("<updated>");
+ html_txt(show_date(info->committer_date, 0,
+ date_mode_from_type(DATE_ISO8601_STRICT)));
+ html("</updated>\n");
+ html("<author>\n");
+ if (info->author) {
+ html("<name>");
+ html_txt(info->author);
+ html("</name>\n");
+ }
+ if (info->author_email && !ctx.cfg.noplainemail) {
+ mail = xstrdup(info->author_email);
+ t = strchr(mail, '<');
+ if (t)
+ t++;
+ else
+ t = mail;
+ t2 = strchr(t, '>');
+ if (t2)
+ *t2 = '\0';
+ html("<email>");
+ html_txt(t);
+ html("</email>\n");
+ free(mail);
+ }
+ html("</author>\n");
+ html("<published>");
+ html_txt(show_date(info->author_date, 0,
+ date_mode_from_type(DATE_ISO8601_STRICT)));
+ html("</published>\n");
+ if (host) {
+ char *pageurl;
+ 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);
+ if (ctx.cfg.virtual_root)
+ delim = '?';
+ html_attrf("%cid=%s", delim, hex);
+ html("'/>\n");
+ free(pageurl);
+ }
+ html("<id>");
+ html_txtf("urn:%s:%s", the_hash_algo->name, hex);
+ html("</id>\n");
+ html("<content type='text'>\n");
+ html_txt(info->msg);
+ html("</content>\n");
+ html("</entry>\n");
+ cgit_free_commitinfo(info);
+}
+
+
+void cgit_print_atom(char *tip, const char *path, int max_count)
+{
+ char *host;
+ const char *argv[] = {NULL, tip, NULL, NULL, NULL};
+ struct commit *commit;
+ struct rev_info rev;
+ int argc = 2;
+ bool first = true;
+
+ if (ctx.qry.show_all)
+ argv[1] = "--all";
+ else if (!tip)
+ argv[1] = ctx.qry.head;
+
+ if (path) {
+ argv[argc++] = "--";
+ argv[argc++] = path;
+ }
+
+ 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);
+ prepare_revision_walk(&rev);
+
+ host = cgit_hosturl();
+ ctx.page.mimetype = "text/xml";
+ ctx.page.charset = "utf-8";
+ cgit_print_http_headers();
+ html("<feed xmlns='http://www.w3.org/2005/Atom'>\n");
+ html("<title>");
+ html_txt(ctx.repo->name);
+ if (path) {
+ html("/");
+ html_txt(path);
+ }
+ if (tip && !ctx.qry.show_all) {
+ html(", branch ");
+ html_txt(tip);
+ }
+ html("</title>\n");
+ html("<subtitle>");
+ html_txt(ctx.repo->desc);
+ html("</subtitle>\n");
+ if (host) {
+ char *fullurl = cgit_currentfullurl();
+ char *repourl = cgit_repourl(ctx.repo->url);
+ html("<id>");
+ html_txtf("%s%s%s", cgit_httpscheme(), host, fullurl);
+ html("</id>\n");
+ html("<link rel='self' href='");
+ html_attrf("%s%s%s", cgit_httpscheme(), host, fullurl);
+ html("'/>\n");
+ html("<link rel='alternate' type='text/html' href='");
+ html_attrf("%s%s%s", cgit_httpscheme(), host, repourl);
+ html("'/>\n");
+ free(fullurl);
+ free(repourl);
+ }
+ while ((commit = get_revision(&rev)) != NULL) {
+ if (first) {
+ html("<updated>");
+ html_txt(show_date(commit->date, 0,
+ date_mode_from_type(DATE_ISO8601_STRICT)));
+ html("</updated>\n");
+ first = false;
+ }
+ add_entry(commit, host);
+ release_commit_memory(the_repository->parsed_objects, commit);
+ commit->parents = NULL;
+ }
+ html("</feed>\n");
+ free(host);
+}
diff --git a/source/ui-atom.h b/source/ui-atom.h
new file mode 100644
index 0000000..dda953b
--- /dev/null
+++ b/source/ui-atom.h
@@ -0,0 +1,6 @@
+#ifndef UI_ATOM_H
+#define UI_ATOM_H
+
+extern void cgit_print_atom(char *tip, const char *path, int max_count);
+
+#endif
diff --git a/source/ui-blame.c b/source/ui-blame.c
new file mode 100644
index 0000000..5c6f36e
--- /dev/null
+++ b/source/ui-blame.c
@@ -0,0 +1,317 @@
+/* ui-blame.c: functions for blame output
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-blame.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "strvec.h"
+#include "blame.h"
+
+
+static char *emit_suspect_detail(struct blame_origin *suspect)
+{
+ struct commitinfo *info;
+ struct strbuf detail = STRBUF_INIT;
+
+ info = cgit_parse_commit(suspect->commit);
+
+ strbuf_addf(&detail, "author %s", info->author);
+ if (!ctx.cfg.noplainemail)
+ strbuf_addf(&detail, " %s", info->author_email);
+ strbuf_addf(&detail, " %s\n",
+ show_date(info->author_date, info->author_tz,
+ cgit_date_mode(DATE_ISO8601)));
+
+ strbuf_addf(&detail, "committer %s", info->committer);
+ if (!ctx.cfg.noplainemail)
+ strbuf_addf(&detail, " %s", info->committer_email);
+ strbuf_addf(&detail, " %s\n\n",
+ show_date(info->committer_date, info->committer_tz,
+ cgit_date_mode(DATE_ISO8601)));
+
+ strbuf_addstr(&detail, info->subject);
+
+ cgit_free_commitinfo(info);
+ return strbuf_detach(&detail, NULL);
+}
+
+static void emit_blame_entry_hash(struct blame_entry *ent)
+{
+ struct blame_origin *suspect = ent->suspect;
+ struct object_id *oid = &suspect->commit->object.oid;
+ unsigned long line = 0;
+
+ char *detail = emit_suspect_detail(suspect);
+ html("<span class='oid'>");
+ cgit_commit_link(repo_find_unique_abbrev(the_repository, oid, DEFAULT_ABBREV), detail,
+ NULL, ctx.qry.head, oid_to_hex(oid), suspect->path);
+ html("</span>");
+ free(detail);
+
+ if (!repo_parse_commit(the_repository, suspect->commit) && suspect->commit->parents) {
+ struct commit *parent = suspect->commit->parents->item;
+
+ html(" ");
+ cgit_blame_link("^", "Blame the previous revision", NULL,
+ ctx.qry.head, oid_to_hex(&parent->object.oid),
+ suspect->path);
+ }
+
+ while (line++ < ent->num_lines)
+ html("\n");
+}
+
+static void emit_blame_entry_linenumber(struct blame_entry *ent)
+{
+ const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n";
+
+ unsigned long lineno = ent->lno;
+ while (lineno < ent->lno + ent->num_lines)
+ htmlf(numberfmt, ++lineno);
+}
+
+static void emit_blame_entry_line_background(struct blame_scoreboard *sb,
+ struct blame_entry *ent)
+{
+ unsigned long line;
+ size_t len, maxlen = 2;
+ const char* pos, *endpos;
+
+ for (line = ent->lno; line < ent->lno + ent->num_lines; line++) {
+ html("\n");
+ pos = blame_nth_line(sb, line);
+ endpos = blame_nth_line(sb, line + 1);
+ len = 0;
+ while (pos < endpos) {
+ len++;
+ if (*pos++ == '\t')
+ len = (len + 7) & ~7;
+ }
+ if (len > maxlen)
+ maxlen = len;
+ }
+
+ for (len = 0; len < maxlen - 1; len++)
+ html(" ");
+}
+
+struct walk_tree_context {
+ char *curr_rev;
+ int match_baselen;
+ int state;
+};
+
+static void print_object(const struct object_id *oid, const char *path,
+ const char *basename, const char *rev)
+{
+ enum object_type type;
+ char *buf;
+ unsigned long size;
+ struct strvec rev_argv = STRVEC_INIT;
+ struct rev_info revs;
+ struct blame_scoreboard sb;
+ struct blame_origin *o;
+ struct blame_entry *ent = NULL;
+
+ type = odb_read_object_info(the_repository->objects, oid, &size);
+ if (type == OBJ_BAD) {
+ cgit_print_error_page(404, "Not found", "Bad object name: %s",
+ oid_to_hex(oid));
+ return;
+ }
+
+ buf = odb_read_object(the_repository->objects, oid, &type, &size);
+ if (!buf) {
+ cgit_print_error_page(500, "Internal server error",
+ "Error reading object %s", oid_to_hex(oid));
+ return;
+ }
+
+ strvec_push(&rev_argv, "blame");
+ strvec_push(&rev_argv, rev);
+ repo_init_revisions(the_repository, &revs, NULL);
+ revs.diffopt.flags.allow_textconv = 1;
+ setup_revisions(rev_argv.nr, rev_argv.v, &revs, NULL);
+ init_scoreboard(&sb);
+ sb.revs = &revs;
+ sb.repo = the_repository;
+ sb.path = path;
+ setup_scoreboard(&sb, &o);
+ o->suspects = blame_entry_prepend(NULL, 0, sb.num_lines, o);
+ prio_queue_put(&sb.commits, o->commit);
+ blame_origin_decref(o);
+ sb.ent = NULL;
+ sb.path = path;
+ assign_blame(&sb, 0);
+ blame_sort_final(&sb);
+ blame_coalesce(&sb);
+
+ cgit_set_title_from_path(path);
+
+ cgit_print_layout_start();
+ htmlf("blob: %s (", oid_to_hex(oid));
+ cgit_plain_link("plain", NULL, NULL, ctx.qry.head, rev, path);
+ html(") (");
+ cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path);
+ html(")\n");
+
+ if (buffer_is_binary(buf, size)) {
+ html("<div class='error'>blob is binary.</div>");
+ goto cleanup;
+ }
+ if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) {
+ htmlf("<div class='error'>blob size (%ldKB)"
+ " exceeds display size limit (%dKB).</div>",
+ size / 1024, ctx.cfg.max_blob_size);
+ goto cleanup;
+ }
+
+ html("<table class='blame blob'>\n<tr>\n");
+
+ /* Commit hashes */
+ html("<td class='hashes'>");
+ for (ent = sb.ent; ent; ent = ent->next) {
+ html("<div class='alt'><pre>");
+ emit_blame_entry_hash(ent);
+ html("</pre></div>");
+ }
+ html("</td>\n");
+
+ /* Line numbers */
+ if (ctx.cfg.enable_tree_linenumbers) {
+ html("<td class='linenumbers'>");
+ for (ent = sb.ent; ent; ent = ent->next) {
+ html("<div class='alt'><pre>");
+ emit_blame_entry_linenumber(ent);
+ html("</pre></div>");
+ }
+ html("</td>\n");
+ }
+
+ html("<td class='lines'><div>");
+
+ /* Colored bars behind lines */
+ html("<div>");
+ for (ent = sb.ent; ent; ) {
+ struct blame_entry *e = ent->next;
+ html("<div class='alt'><pre>");
+ emit_blame_entry_line_background(&sb, ent);
+ html("</pre></div>");
+ free(ent);
+ ent = e;
+ }
+ html("</div>");
+
+ free((void *)sb.final_buf);
+
+ /* Lines */
+ html("<pre><code>");
+ if (ctx.repo->source_filter) {
+ char *filter_arg = xstrdup(basename);
+ cgit_open_filter(ctx.repo->source_filter, filter_arg);
+ html_raw(buf, size);
+ cgit_close_filter(ctx.repo->source_filter);
+ free(filter_arg);
+ } else {
+ html_txt(buf);
+ }
+ html("</code></pre>");
+
+ html("</div></td>\n");
+
+ html("</tr>\n</table>\n");
+
+ cgit_print_layout_end();
+
+cleanup:
+ free(buf);
+}
+
+static int walk_tree(const struct object_id *oid, struct strbuf *base,
+ const char *pathname, unsigned mode, void *cbdata)
+{
+ struct walk_tree_context *walk_tree_ctx = cbdata;
+
+ if (base->len == walk_tree_ctx->match_baselen) {
+ if (S_ISREG(mode)) {
+ struct strbuf buffer = STRBUF_INIT;
+ strbuf_addbuf(&buffer, base);
+ strbuf_addstr(&buffer, pathname);
+ print_object(oid, buffer.buf, pathname,
+ walk_tree_ctx->curr_rev);
+ strbuf_release(&buffer);
+ walk_tree_ctx->state = 1;
+ } else if (S_ISDIR(mode)) {
+ walk_tree_ctx->state = 2;
+ }
+ } else if (base->len < INT_MAX
+ && (int)base->len > walk_tree_ctx->match_baselen) {
+ walk_tree_ctx->state = 2;
+ } else if (S_ISDIR(mode)) {
+ return READ_TREE_RECURSIVE;
+ }
+ return 0;
+}
+
+static int basedir_len(const char *path)
+{
+ const char *p = strrchr(path, '/');
+ if (p)
+ return p - path + 1;
+ return 0;
+}
+
+void cgit_print_blame(void)
+{
+ const char *rev = ctx.qry.oid;
+ struct object_id oid;
+ struct commit *commit;
+ struct pathspec_item path_items = {
+ .match = ctx.qry.path,
+ .len = ctx.qry.path ? strlen(ctx.qry.path) : 0
+ };
+ struct pathspec paths = {
+ .nr = 1,
+ .items = &path_items
+ };
+ struct walk_tree_context walk_tree_ctx = {
+ .state = 0
+ };
+
+ if (!rev)
+ rev = ctx.qry.head;
+
+ if (repo_get_oid(the_repository, rev, &oid)) {
+ cgit_print_error_page(404, "Not found",
+ "Invalid revision name: %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",
+ "Invalid commit reference: %s", rev);
+ return;
+ }
+
+ walk_tree_ctx.curr_rev = xstrdup(rev);
+ walk_tree_ctx.match_baselen = (path_items.match) ?
+ basedir_len(path_items.match) : -1;
+
+ read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+ &paths, walk_tree, &walk_tree_ctx);
+ if (!walk_tree_ctx.state)
+ cgit_print_error_page(404, "Not found", "Not found");
+ else if (walk_tree_ctx.state == 2)
+ cgit_print_error_page(404, "No blame for folders",
+ "Blame is not available for folders.");
+
+ free(walk_tree_ctx.curr_rev);
+}
diff --git a/source/ui-blame.h b/source/ui-blame.h
new file mode 100644
index 0000000..5b97e03
--- /dev/null
+++ b/source/ui-blame.h
@@ -0,0 +1,6 @@
+#ifndef UI_BLAME_H
+#define UI_BLAME_H
+
+extern void cgit_print_blame(void);
+
+#endif /* UI_BLAME_H */
diff --git a/source/ui-blob.c b/source/ui-blob.c
new file mode 100644
index 0000000..bc91656
--- /dev/null
+++ b/source/ui-blob.c
@@ -0,0 +1,184 @@
+/* ui-blob.c: show blob content
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-blob.h"
+#include "html.h"
+#include "ui-shared.h"
+
+struct walk_tree_context {
+ const char *match_path;
+ struct object_id *matched_oid;
+ unsigned int found_path:1;
+ unsigned int file_only:1;
+};
+
+static int walk_tree(const struct object_id *oid, struct strbuf *base,
+ const char *pathname, unsigned mode, void *cbdata)
+{
+ struct walk_tree_context *walk_tree_ctx = cbdata;
+
+ if (walk_tree_ctx->file_only && !S_ISREG(mode))
+ return READ_TREE_RECURSIVE;
+ if (strncmp(base->buf, walk_tree_ctx->match_path, base->len)
+ || strcmp(walk_tree_ctx->match_path + base->len, pathname))
+ return READ_TREE_RECURSIVE;
+ oidcpy(walk_tree_ctx->matched_oid, oid);
+ walk_tree_ctx->found_path = 1;
+ return 0;
+}
+
+int cgit_ref_path_exists(const char *path, const char *ref, int file_only)
+{
+ struct object_id oid;
+ unsigned long size;
+ struct pathspec_item path_items = {
+ .match = xstrdup(path),
+ .len = strlen(path)
+ };
+ struct pathspec paths = {
+ .nr = 1,
+ .items = &path_items
+ };
+ struct walk_tree_context walk_tree_ctx = {
+ .match_path = path,
+ .matched_oid = &oid,
+ .found_path = 0,
+ .file_only = file_only
+ };
+
+ if (repo_get_oid(the_repository, ref, &oid))
+ goto done;
+ if (odb_read_object_info(the_repository->objects, &oid, &size) != OBJ_COMMIT)
+ goto done;
+ read_tree(the_repository,
+ repo_get_commit_tree(the_repository, lookup_commit_reference(the_repository, &oid)),
+ &paths, walk_tree, &walk_tree_ctx);
+
+done:
+ free(path_items.match);
+ return walk_tree_ctx.found_path;
+}
+
+int cgit_print_file(char *path, const char *head, int file_only)
+{
+ struct object_id oid;
+ enum object_type type;
+ char *buf;
+ unsigned long size;
+ struct commit *commit;
+ struct pathspec_item path_items = {
+ .match = path,
+ .len = strlen(path)
+ };
+ struct pathspec paths = {
+ .nr = 1,
+ .items = &path_items
+ };
+ struct walk_tree_context walk_tree_ctx = {
+ .match_path = path,
+ .matched_oid = &oid,
+ .found_path = 0,
+ .file_only = file_only
+ };
+
+ if (repo_get_oid(the_repository, head, &oid))
+ return -1;
+ type = odb_read_object_info(the_repository->objects, &oid, &size);
+ if (type == OBJ_COMMIT) {
+ commit = lookup_commit_reference(the_repository, &oid);
+ read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+ &paths, walk_tree, &walk_tree_ctx);
+ if (!walk_tree_ctx.found_path)
+ return -1;
+ type = odb_read_object_info(the_repository->objects, &oid, &size);
+ }
+ if (type == OBJ_BAD)
+ return -1;
+ buf = odb_read_object(the_repository->objects, &oid, &type, &size);
+ if (!buf)
+ return -1;
+ buf[size] = '\0';
+ html_raw(buf, size);
+ free(buf);
+ return 0;
+}
+
+void cgit_print_blob(const char *hex, char *path, const char *head, int file_only)
+{
+ struct object_id oid;
+ enum object_type type;
+ char *buf;
+ unsigned long size;
+ struct commit *commit;
+ struct pathspec_item path_items = {
+ .match = path,
+ .len = path ? strlen(path) : 0
+ };
+ struct pathspec paths = {
+ .nr = 1,
+ .items = &path_items
+ };
+ struct walk_tree_context walk_tree_ctx = {
+ .match_path = path,
+ .matched_oid = &oid,
+ .found_path = 0,
+ .file_only = file_only
+ };
+
+ if (hex) {
+ if (get_oid_hex(hex, &oid)) {
+ cgit_print_error_page(400, "Bad request",
+ "Bad hex value: %s", hex);
+ return;
+ }
+ } else {
+ if (repo_get_oid(the_repository, head, &oid)) {
+ cgit_print_error_page(404, "Not found",
+ "Bad ref: %s", head);
+ return;
+ }
+ }
+
+ type = odb_read_object_info(the_repository->objects, &oid, &size);
+
+ if ((!hex) && type == OBJ_COMMIT && path) {
+ commit = lookup_commit_reference(the_repository, &oid);
+ read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+ &paths, walk_tree, &walk_tree_ctx);
+ type = odb_read_object_info(the_repository->objects, &oid, &size);
+ }
+
+ if (type == OBJ_BAD) {
+ cgit_print_error_page(404, "Not found",
+ "Bad object name: %s", hex);
+ return;
+ }
+
+ buf = odb_read_object(the_repository->objects, &oid, &type, &size);
+ if (!buf) {
+ cgit_print_error_page(500, "Internal server error",
+ "Error reading object %s", hex);
+ return;
+ }
+
+ buf[size] = '\0';
+ if (buffer_is_binary(buf, size))
+ ctx.page.mimetype = "application/octet-stream";
+ else
+ ctx.page.mimetype = "text/plain";
+ ctx.page.filename = path;
+
+ html("X-Content-Type-Options: nosniff\n");
+ html("Content-Security-Policy: default-src 'none'\n");
+ cgit_print_http_headers();
+ html_raw(buf, size);
+ free(buf);
+}
diff --git a/source/ui-blob.h b/source/ui-blob.h
new file mode 100644
index 0000000..16847b2
--- /dev/null
+++ b/source/ui-blob.h
@@ -0,0 +1,8 @@
+#ifndef UI_BLOB_H
+#define UI_BLOB_H
+
+extern int cgit_ref_path_exists(const char *path, const char *ref, int file_only);
+extern int cgit_print_file(char *path, const char *head, int file_only);
+extern void cgit_print_blob(const char *hex, char *path, const char *head, int file_only);
+
+#endif /* UI_BLOB_H */
diff --git a/source/ui-clone.c b/source/ui-clone.c
new file mode 100644
index 0000000..e11f496
--- /dev/null
+++ b/source/ui-clone.c
@@ -0,0 +1,138 @@
+/* ui-clone.c: functions for http cloning, based on
+ * git's http-backend.c by Shawn O. Pearce
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-clone.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "packfile.h"
+
+static int print_ref_info(const struct reference *ref, void *cb_data)
+{
+ struct object *obj;
+
+ if (!(obj = parse_object(the_repository, ref->oid)))
+ return 0;
+
+ htmlf("%s\t%s\n", oid_to_hex(ref->oid), ref->name);
+ if (obj->type == OBJ_TAG) {
+ if (!(obj = deref_tag(the_repository, obj, ref->name, 0)))
+ return 0;
+ htmlf("%s\t%s^{}\n", oid_to_hex(&obj->oid), ref->name);
+ }
+ return 0;
+}
+
+static void print_pack_info(void)
+{
+ struct odb_source *source;
+ char *offset;
+
+ ctx.page.mimetype = "text/plain";
+ ctx.page.filename = "objects/info/packs";
+ cgit_print_http_headers();
+ odb_reprepare(the_repository->objects);
+ for (source = the_repository->objects->sources; source; source = source->next) {
+ struct odb_source_files *files = odb_source_files_downcast(source);
+ struct packfile_list_entry *e;
+ for (e = files->packed->packs.head; e; e = e->next) {
+ struct packed_git *p = e->pack;
+ if (p->pack_local) {
+ offset = strrchr(p->pack_name, '/');
+ if (offset && offset[1] != '\0')
+ ++offset;
+ else
+ offset = p->pack_name;
+ htmlf("P %s\n", offset);
+ }
+ }
+ }
+}
+
+static void send_file(const char *path)
+{
+ struct stat st;
+
+ if (stat(path, &st)) {
+ switch (errno) {
+ case ENOENT:
+ cgit_print_error_page(404, "Not found", "Not found");
+ break;
+ case EACCES:
+ cgit_print_error_page(403, "Forbidden", "Forbidden");
+ break;
+ default:
+ cgit_print_error_page(400, "Bad request", "Bad request");
+ }
+ return;
+ }
+ ctx.page.mimetype = "application/octet-stream";
+ ctx.page.filename = path;
+ skip_prefix(path, ctx.repo->path, &ctx.page.filename);
+ skip_prefix(ctx.page.filename, "/", &ctx.page.filename);
+ cgit_print_http_headers();
+ html_include(path);
+}
+
+void cgit_clone_info(void)
+{
+ if (!ctx.qry.path || strcmp(ctx.qry.path, "refs")) {
+ cgit_print_error_page(400, "Bad request", "Bad request");
+ return;
+ }
+
+ ctx.page.mimetype = "text/plain";
+ ctx.page.filename = "info/refs";
+ cgit_print_http_headers();
+ refs_for_each_ref(get_main_ref_store(the_repository),
+ print_ref_info, NULL);
+}
+
+void cgit_clone_objects(void)
+{
+ char *p, *path;
+
+ if (!ctx.qry.path)
+ goto err;
+
+ if (!strcmp(ctx.qry.path, "info/packs")) {
+ print_pack_info();
+ return;
+ }
+
+ /* Avoid directory traversal by forbidding "..", but also work around
+ * other funny business by just specifying a fairly strict format. For
+ * example, now we don't have to stress out about the Cygwin port.
+ */
+ for (p = ctx.qry.path; *p; ++p) {
+ if (*p == '.' && *(p + 1) == '.')
+ goto err;
+ if (!isalnum(*p) && *p != '/' && *p != '.' && *p != '-')
+ goto err;
+ }
+
+ 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");
+}
+
+void cgit_clone_head(void)
+{
+ char *path;
+
+ path = repo_git_path(the_repository, "HEAD");
+ send_file(path);
+ free(path);
+}
diff --git a/source/ui-clone.h b/source/ui-clone.h
new file mode 100644
index 0000000..3e460a3
--- /dev/null
+++ b/source/ui-clone.h
@@ -0,0 +1,8 @@
+#ifndef UI_CLONE_H
+#define UI_CLONE_H
+
+void cgit_clone_info(void);
+void cgit_clone_objects(void);
+void cgit_clone_head(void);
+
+#endif /* UI_CLONE_H */
diff --git a/source/ui-commit.c b/source/ui-commit.c
new file mode 100644
index 0000000..972e9bc
--- /dev/null
+++ b/source/ui-commit.c
@@ -0,0 +1,150 @@
+/* ui-commit.c: generate commit view
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-commit.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "ui-diff.h"
+#include "ui-log.h"
+
+void cgit_print_commit(char *hex, const char *prefix)
+{
+ struct commit *commit, *parent;
+ struct commitinfo *info, *parent_info;
+ struct commit_list *p;
+ struct strbuf notes = STRBUF_INIT;
+ struct object_id oid;
+ char *tmp, *tmp2;
+ int parents = 0;
+
+ if (!hex)
+ hex = ctx.qry.head;
+
+ if (repo_get_oid(the_repository, hex, &oid)) {
+ cgit_print_error_page(400, "Bad request",
+ "Bad object id: %s", hex);
+ return;
+ }
+ commit = lookup_commit_reference(the_repository, &oid);
+ if (!commit) {
+ cgit_print_error_page(404, "Not found",
+ "Bad commit reference: %s", hex);
+ return;
+ }
+ info = cgit_parse_commit(commit);
+
+ format_display_notes(&oid, &notes, PAGE_ENCODING, 1);
+
+ load_ref_decorations(NULL, DECORATE_FULL_REFS);
+
+ ctx.page.title = fmtalloc("%s - %s", info->subject, ctx.page.title);
+ cgit_print_layout_start();
+ cgit_print_diff_ctrls();
+ html("<table summary='commit info' class='commit-info'>\n");
+ html("<tr><th>author</th><td>");
+ cgit_open_filter(ctx.repo->email_filter, info->author_email, "commit");
+ html_txt(info->author);
+ if (!ctx.cfg.noplainemail) {
+ html(" ");
+ html_txt(info->author_email);
+ }
+ cgit_close_filter(ctx.repo->email_filter);
+ html("</td><td class='right'>");
+ html_txt(show_date(info->author_date, info->author_tz,
+ cgit_date_mode(DATE_ISO8601)));
+ html("</td></tr>\n");
+ html("<tr><th>committer</th><td>");
+ cgit_open_filter(ctx.repo->email_filter, info->committer_email, "commit");
+ html_txt(info->committer);
+ if (!ctx.cfg.noplainemail) {
+ html(" ");
+ html_txt(info->committer_email);
+ }
+ cgit_close_filter(ctx.repo->email_filter);
+ html("</td><td class='right'>");
+ html_txt(show_date(info->committer_date, info->committer_tz,
+ cgit_date_mode(DATE_ISO8601)));
+ html("</td></tr>\n");
+ html("<tr><th>commit</th><td colspan='2' class='oid'>");
+ tmp = oid_to_hex(&commit->object.oid);
+ cgit_commit_link(tmp, NULL, NULL, ctx.qry.head, tmp, prefix);
+ html(" (");
+ cgit_patch_link("patch", NULL, NULL, NULL, tmp, prefix);
+ html(")</td></tr>\n");
+ html("<tr><th>tree</th><td colspan='2' class='oid'>");
+ tmp = xstrdup(hex);
+ cgit_tree_link(oid_to_hex(get_commit_tree_oid(commit)), NULL, NULL,
+ ctx.qry.head, tmp, NULL);
+ if (prefix) {
+ html(" /");
+ cgit_tree_link(prefix, NULL, NULL, ctx.qry.head, tmp, prefix);
+ }
+ free(tmp);
+ html("</td></tr>\n");
+ for (p = commit->parents; p; p = p->next) {
+ parent = lookup_commit_reference(the_repository, &p->item->object.oid);
+ if (!parent) {
+ html("<tr><td colspan='3'>");
+ cgit_print_error("Error reading parent commit");
+ html("</td></tr>");
+ continue;
+ }
+ html("<tr><th>parent</th>"
+ "<td colspan='2' class='oid'>");
+ tmp = tmp2 = oid_to_hex(&p->item->object.oid);
+ if (ctx.repo->enable_subject_links) {
+ parent_info = cgit_parse_commit(parent);
+ tmp2 = parent_info->subject;
+ }
+ cgit_commit_link(tmp2, NULL, NULL, ctx.qry.head, tmp, prefix);
+ html(" (");
+ cgit_diff_link("diff", NULL, NULL, ctx.qry.head, hex,
+ oid_to_hex(&p->item->object.oid), prefix);
+ html(")</td></tr>");
+ parents++;
+ }
+ if (ctx.repo->snapshots) {
+ html("<tr><th>download</th><td colspan='2' class='oid'>");
+ cgit_print_snapshot_links(ctx.repo, hex, "<br/>");
+ html("</td></tr>");
+ }
+ html("</table>\n");
+ html("<div class='commit-subject'>");
+ cgit_open_filter(ctx.repo->commit_filter);
+ html_txt(info->subject);
+ cgit_close_filter(ctx.repo->commit_filter);
+ show_commit_decorations(commit);
+ html("</div>");
+ html("<div class='commit-msg'>");
+ cgit_open_filter(ctx.repo->commit_filter);
+ html_txt(info->msg);
+ cgit_close_filter(ctx.repo->commit_filter);
+ html("</div>");
+ if (notes.len != 0) {
+ html("<div class='notes-header'>Notes</div>");
+ html("<div class='notes'>");
+ cgit_open_filter(ctx.repo->commit_filter);
+ html_txt(notes.buf);
+ cgit_close_filter(ctx.repo->commit_filter);
+ html("</div>");
+ html("<div class='notes-footer'></div>");
+ }
+ if (parents < 3) {
+ if (parents)
+ tmp = oid_to_hex(&commit->parents->item->object.oid);
+ else
+ tmp = NULL;
+ cgit_print_diff(ctx.qry.oid, tmp, prefix, 0, 0);
+ }
+ strbuf_release(&notes);
+ cgit_free_commitinfo(info);
+ cgit_print_layout_end();
+}
diff --git a/source/ui-commit.h b/source/ui-commit.h
new file mode 100644
index 0000000..8198b4b
--- /dev/null
+++ b/source/ui-commit.h
@@ -0,0 +1,6 @@
+#ifndef UI_COMMIT_H
+#define UI_COMMIT_H
+
+extern void cgit_print_commit(char *hex, const char *prefix);
+
+#endif /* UI_COMMIT_H */
diff --git a/source/ui-diff.c b/source/ui-diff.c
new file mode 100644
index 0000000..6cfa697
--- /dev/null
+++ b/source/ui-diff.c
This diff is too large to be rendered inline. View it on its own page.
diff --git a/source/ui-diff.h b/source/ui-diff.h
new file mode 100644
index 0000000..39264a1
--- /dev/null
+++ b/source/ui-diff.h
@@ -0,0 +1,15 @@
+#ifndef UI_DIFF_H
+#define UI_DIFF_H
+
+extern void cgit_print_diff_ctrls(void);
+
+extern void cgit_print_diff(const char *new_hex, const char *old_hex,
+ 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];
+
+#endif /* UI_DIFF_H */
diff --git a/source/ui-log.c b/source/ui-log.c
new file mode 100644
index 0000000..9906bbc
--- /dev/null
+++ b/source/ui-log.c
This diff is too large to be rendered inline. View it on its own page.
diff --git a/source/ui-log.h b/source/ui-log.h
new file mode 100644
index 0000000..325607c
--- /dev/null
+++ b/source/ui-log.h
@@ -0,0 +1,9 @@
+#ifndef UI_LOG_H
+#define UI_LOG_H
+
+extern void cgit_print_log(const char *tip, int ofs, int cnt, char *grep,
+ char *pattern, const char *path, int pager,
+ int commit_graph, int commit_sort);
+extern void show_commit_decorations(struct commit *commit);
+
+#endif /* UI_LOG_H */
diff --git a/source/ui-patch.c b/source/ui-patch.c
new file mode 100644
index 0000000..f9d2eeb
--- /dev/null
+++ b/source/ui-patch.c
@@ -0,0 +1,100 @@
+/* ui-patch.c: generate patch view
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-patch.h"
+#include "html.h"
+#include "ui-shared.h"
+
+/* two commit hashes with two dots in between and termination */
+#define REV_RANGE_LEN 2 * GIT_MAX_HEXSZ + 3
+
+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_rev_oid, old_rev_oid;
+ char rev_range[REV_RANGE_LEN];
+ const char *rev_argv[] = { NULL, "--reverse", "--format=email", rev_range, "--", prefix, NULL };
+ int rev_argc = ARRAY_SIZE(rev_argv) - 1;
+ char *patchname;
+
+ if (!prefix)
+ rev_argc--;
+
+ 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;
+ }
+ commit = lookup_commit_reference(the_repository, &new_rev_oid);
+ if (!commit) {
+ cgit_print_error_page(404, "Not found",
+ "Bad commit reference: %s", new_rev);
+ return;
+ }
+
+ 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;
+ }
+ if (!lookup_commit_reference(the_repository, &old_rev_oid)) {
+ cgit_print_error_page(404, "Not found",
+ "Bad commit reference: %s", old_rev);
+ return;
+ }
+ } else if (commit->parents && commit->parents->item) {
+ oidcpy(&old_rev_oid, &commit->parents->item->object.oid);
+ } else {
+ oidclr(&old_rev_oid, the_repository->hash_algo);
+ }
+
+ if (is_null_oid(&old_rev_oid)) {
+ memcpy(rev_range, oid_to_hex(&new_rev_oid), the_hash_algo->hexsz + 1);
+ } else {
+ xsnprintf(rev_range, REV_RANGE_LEN, "%s..%s", oid_to_hex(&old_rev_oid),
+ oid_to_hex(&new_rev_oid));
+ }
+
+ patchname = fmt("%s.patch", rev_range);
+ ctx.page.mimetype = "text/plain";
+ ctx.page.filename = patchname;
+ cgit_print_http_headers();
+
+ if (ctx.cfg.noplainemail) {
+ rev_argv[2] = "--format=format:From %H Mon Sep 17 00:00:00 "
+ "2001%nFrom: %an%nDate: %aD%n%w(78,0,1)Subject: "
+ "%s%n%n%w(0)%b";
+ }
+
+ repo_init_revisions(the_repository, &rev, NULL);
+ rev.abbrev = DEFAULT_ABBREV;
+ rev.verbose_header = 1;
+ rev.diff = 1;
+ rev.show_root_diff = 1;
+ rev.max_parents = 1;
+ rev.diffopt.output_format |= DIFF_FORMAT_DIFFSTAT |
+ DIFF_FORMAT_PATCH | DIFF_FORMAT_SUMMARY;
+ if (prefix)
+ rev.diffopt.stat_sep = fmt("(limited to '%s')\n\n", prefix);
+ setup_revisions(rev_argc, rev_argv, &rev, NULL);
+ prepare_revision_walk(&rev);
+
+ while ((commit = get_revision(&rev)) != NULL) {
+ log_tree_commit(&rev, commit);
+ printf("-- \ncgit %s\n\n", cgit_version);
+ }
+}
diff --git a/source/ui-patch.h b/source/ui-patch.h
new file mode 100644
index 0000000..7a6cacd
--- /dev/null
+++ b/source/ui-patch.h
@@ -0,0 +1,7 @@
+#ifndef UI_PATCH_H
+#define UI_PATCH_H
+
+extern void cgit_print_patch(const char *new_rev, const char *old_rev,
+ const char *prefix);
+
+#endif /* UI_PATCH_H */
diff --git a/source/ui-plain.c b/source/ui-plain.c
new file mode 100644
index 0000000..a2a4087
--- /dev/null
+++ b/source/ui-plain.c
@@ -0,0 +1,209 @@
+/* ui-plain.c: functions for output of plain blobs by path
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-plain.h"
+#include "html.h"
+#include "ui-shared.h"
+
+struct walk_tree_context {
+ int match_baselen;
+ int match;
+};
+
+static int print_object(const struct object_id *oid, const char *path)
+{
+ enum object_type type;
+ char *buf, *mimetype;
+ unsigned long size;
+
+ type = odb_read_object_info(the_repository->objects, oid, &size);
+ if (type == OBJ_BAD) {
+ cgit_print_error_page(404, "Not found", "Not found");
+ return 0;
+ }
+
+ buf = odb_read_object(the_repository->objects, oid, &type, &size);
+ if (!buf) {
+ cgit_print_error_page(404, "Not found", "Not found");
+ return 0;
+ }
+
+ mimetype = get_mimetype_for_filename(path);
+ ctx.page.mimetype = mimetype;
+
+ if (!ctx.repo->enable_html_serving) {
+ html("X-Content-Type-Options: nosniff\n");
+ html("Content-Security-Policy: default-src 'none'\n");
+ if (mimetype) {
+ /* Built-in white list allows PDF and everything that isn't text/ and application/ */
+ if ((!strncmp(mimetype, "text/", 5) || !strncmp(mimetype, "application/", 12)) && strcmp(mimetype, "application/pdf"))
+ ctx.page.mimetype = NULL;
+ }
+ }
+
+ if (!ctx.page.mimetype) {
+ if (buffer_is_binary(buf, size)) {
+ ctx.page.mimetype = "application/octet-stream";
+ ctx.page.charset = NULL;
+ } else {
+ ctx.page.mimetype = "text/plain";
+ }
+ }
+ ctx.page.filename = path;
+ ctx.page.size = size;
+ ctx.page.etag = oid_to_hex(oid);
+ cgit_print_http_headers();
+ html_raw(buf, size);
+ free(mimetype);
+ free(buf);
+ return 1;
+}
+
+static char *buildpath(const char *base, int baselen, const char *path)
+{
+ if (path[0])
+ return fmtalloc("%.*s%s/", baselen, base, path);
+ else
+ return fmtalloc("%.*s/", baselen, base);
+}
+
+static void print_dir(const struct object_id *oid, const char *base,
+ int baselen, const char *path)
+{
+ char *fullpath, *slash;
+ size_t len;
+
+ fullpath = buildpath(base, baselen, path);
+ slash = (fullpath[0] == '/' ? "" : "/");
+ ctx.page.etag = oid_to_hex(oid);
+ cgit_print_http_headers();
+ htmlf("<html><head><title>%s", slash);
+ html_txt(fullpath);
+ htmlf("</title></head>\n<body>\n<h2>%s", slash);
+ html_txt(fullpath);
+ html("</h2>\n<ul>\n");
+ len = strlen(fullpath);
+ if (len > 1) {
+ fullpath[len - 1] = 0;
+ slash = strrchr(fullpath, '/');
+ if (slash)
+ *(slash + 1) = 0;
+ else {
+ free(fullpath);
+ fullpath = NULL;
+ }
+ html("<li>");
+ cgit_plain_link("../", NULL, NULL, ctx.qry.head, ctx.qry.oid,
+ fullpath);
+ html("</li>\n");
+ }
+ free(fullpath);
+}
+
+static void print_dir_entry(const struct object_id *oid, const char *base,
+ int baselen, const char *path, unsigned mode)
+{
+ char *fullpath;
+
+ fullpath = buildpath(base, baselen, path);
+ if (!S_ISDIR(mode) && !S_ISGITLINK(mode))
+ fullpath[strlen(fullpath) - 1] = 0;
+ html(" <li>");
+ if (S_ISGITLINK(mode)) {
+ cgit_submodule_link(NULL, fullpath, oid_to_hex(oid));
+ } else
+ cgit_plain_link(path, NULL, NULL, ctx.qry.head, ctx.qry.oid,
+ fullpath);
+ html("</li>\n");
+ free(fullpath);
+}
+
+static void print_dir_tail(void)
+{
+ html(" </ul>\n</body></html>\n");
+}
+
+static int walk_tree(const struct object_id *oid, struct strbuf *base,
+ const char *pathname, unsigned mode, void *cbdata)
+{
+ struct walk_tree_context *walk_tree_ctx = cbdata;
+
+ if (base->len == walk_tree_ctx->match_baselen) {
+ if (S_ISREG(mode) || S_ISLNK(mode)) {
+ if (print_object(oid, pathname))
+ walk_tree_ctx->match = 1;
+ } else if (S_ISDIR(mode)) {
+ print_dir(oid, base->buf, base->len, pathname);
+ walk_tree_ctx->match = 2;
+ return READ_TREE_RECURSIVE;
+ }
+ } else if (base->len < INT_MAX && (int)base->len > walk_tree_ctx->match_baselen) {
+ print_dir_entry(oid, base->buf, base->len, pathname, mode);
+ walk_tree_ctx->match = 2;
+ } else if (S_ISDIR(mode)) {
+ return READ_TREE_RECURSIVE;
+ }
+
+ return 0;
+}
+
+static int basedir_len(const char *path)
+{
+ const char *p = strrchr(path, '/');
+ if (p)
+ return p - path + 1;
+ return 0;
+}
+
+void cgit_print_plain(void)
+{
+ const char *rev = ctx.qry.oid;
+ struct object_id oid;
+ struct commit *commit;
+ struct pathspec_item path_items = {
+ .match = ctx.qry.path,
+ .len = ctx.qry.path ? strlen(ctx.qry.path) : 0
+ };
+ struct pathspec paths = {
+ .nr = 1,
+ .items = &path_items
+ };
+ struct walk_tree_context walk_tree_ctx = {
+ .match = 0
+ };
+
+ if (!rev)
+ rev = ctx.qry.head;
+
+ if (repo_get_oid(the_repository, rev, &oid)) {
+ cgit_print_error_page(404, "Not found", "Not found");
+ return;
+ }
+ commit = lookup_commit_reference(the_repository, &oid);
+ if (!commit || repo_parse_commit(the_repository, commit)) {
+ cgit_print_error_page(404, "Not found", "Not found");
+ return;
+ }
+ if (!path_items.match) {
+ path_items.match = "";
+ walk_tree_ctx.match_baselen = -1;
+ print_dir(get_commit_tree_oid(commit), "", 0, "");
+ walk_tree_ctx.match = 2;
+ }
+ else
+ walk_tree_ctx.match_baselen = basedir_len(path_items.match);
+ read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+ &paths, walk_tree, &walk_tree_ctx);
+ if (!walk_tree_ctx.match)
+ cgit_print_error_page(404, "Not found", "Not found");
+ else if (walk_tree_ctx.match == 2)
+ print_dir_tail();
+}
diff --git a/source/ui-plain.h b/source/ui-plain.h
new file mode 100644
index 0000000..5bff07b
--- /dev/null
+++ b/source/ui-plain.h
@@ -0,0 +1,6 @@
+#ifndef UI_PLAIN_H
+#define UI_PLAIN_H
+
+extern void cgit_print_plain(void);
+
+#endif /* UI_PLAIN_H */
diff --git a/source/ui-refs.c b/source/ui-refs.c
new file mode 100644
index 0000000..11fb9fc
--- /dev/null
+++ b/source/ui-refs.c
@@ -0,0 +1,224 @@
+/* ui-refs.c: browse symbolic refs
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-refs.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static inline int cmp_age(int age1, int age2)
+{
+ /* age1 and age2 are assumed to be non-negative */
+ return age2 - age1;
+}
+
+static int cmp_ref_name(const void *a, const void *b)
+{
+ struct refinfo *r1 = *(struct refinfo **)a;
+ struct refinfo *r2 = *(struct refinfo **)b;
+
+ return strcmp(r1->refname, r2->refname);
+}
+
+static int cmp_branch_age(const void *a, const void *b)
+{
+ struct refinfo *r1 = *(struct refinfo **)a;
+ struct refinfo *r2 = *(struct refinfo **)b;
+
+ return cmp_age(r1->commit->committer_date, r2->commit->committer_date);
+}
+
+static int get_ref_age(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 0;
+}
+
+static int cmp_tag_age(const void *a, const void *b)
+{
+ struct refinfo *r1 = *(struct refinfo **)a;
+ struct refinfo *r2 = *(struct refinfo **)b;
+
+ return cmp_age(get_ref_age(r1), get_ref_age(r2));
+}
+
+static int print_branch(struct refinfo *ref)
+{
+ struct commitinfo *info = ref->commit;
+ char *name = (char *)ref->refname;
+
+ if (!info)
+ return 1;
+ html("<tr><td>");
+ cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL,
+ ctx.qry.showmsg, 0);
+ html("</td><td>");
+
+ if (ref->object->type == OBJ_COMMIT) {
+ cgit_commit_link(info->subject, NULL, NULL, name, NULL, NULL);
+ html("</td><td>");
+ cgit_open_filter(ctx.repo->email_filter, info->author_email, "refs");
+ html_txt(info->author);
+ cgit_close_filter(ctx.repo->email_filter);
+ html("</td><td colspan='2'>");
+ cgit_print_age(info->committer_date, info->committer_tz, -1);
+ } else {
+ html("</td><td></td><td>");
+ cgit_object_link(ref->object);
+ }
+ html("</td></tr>\n");
+ return 0;
+}
+
+static void print_tag_header(void)
+{
+ html("<tr class='nohover'><th class='left'>Tag</th>"
+ "<th class='left'>Download</th>"
+ "<th class='left'>Author</th>"
+ "<th class='left' colspan='2'>Age</th></tr>\n");
+}
+
+static int print_tag(struct refinfo *ref)
+{
+ struct tag *tag = NULL;
+ struct taginfo *info = NULL;
+ char *name = (char *)ref->refname;
+ struct object *obj = ref->object;
+
+ if (obj->type == OBJ_TAG) {
+ tag = (struct tag *)obj;
+ obj = tag->tagged;
+ info = ref->tag;
+ if (!info)
+ return 1;
+ }
+
+ html("<tr><td>");
+ cgit_tag_link(name, NULL, NULL, name);
+ html("</td><td>");
+ if (ctx.repo->snapshots && (obj->type == OBJ_COMMIT))
+ cgit_print_snapshot_links(ctx.repo, name, "&nbsp;&nbsp;");
+ else
+ cgit_object_link(obj);
+ html("</td><td>");
+ 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);
+ }
+ } else if (ref->object->type == OBJ_COMMIT) {
+ cgit_open_filter(ctx.repo->email_filter, ref->commit->author_email, "refs");
+ html_txt(ref->commit->author);
+ cgit_close_filter(ctx.repo->email_filter);
+ }
+ html("</td><td colspan='2'>");
+ if (info) {
+ if (info->tagger_date > 0)
+ cgit_print_age(info->tagger_date, info->tagger_tz, -1);
+ } else if (ref->object->type == OBJ_COMMIT) {
+ cgit_print_age(ref->commit->commit->date, 0, -1);
+ }
+ html("</td></tr>\n");
+
+ return 0;
+}
+
+static void print_refs_link(const char *path)
+{
+ html("<tr class='nohover'><td colspan='5'>");
+ cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path);
+ html("</td></tr>");
+}
+
+void cgit_print_branches(int maxcount)
+{
+ struct reflist list;
+ int i;
+
+ html("<tr class='nohover'><th class='left'>Branch</th>"
+ "<th class='left'>Commit message</th>"
+ "<th class='left'>Author</th>"
+ "<th class='left' colspan='2'>Age</th></tr>\n");
+
+ list.refs = NULL;
+ list.alloc = list.count = 0;
+ refs_for_each_branch_ref(get_main_ref_store(the_repository),
+ cgit_refs_cb, &list);
+ if (ctx.repo->enable_remote_branches)
+ refs_for_each_remote_ref(get_main_ref_store(the_repository),
+ cgit_refs_cb, &list);
+
+ if (maxcount == 0 || maxcount > list.count)
+ maxcount = list.count;
+
+ qsort(list.refs, list.count, sizeof(*list.refs), cmp_branch_age);
+ if (ctx.repo->branch_sort == 0)
+ qsort(list.refs, maxcount, sizeof(*list.refs), cmp_ref_name);
+
+ for (i = 0; i < maxcount; i++)
+ print_branch(list.refs[i]);
+
+ if (maxcount < list.count)
+ print_refs_link("heads");
+
+ cgit_free_reflist_inner(&list);
+}
+
+void cgit_print_tags(int maxcount)
+{
+ struct reflist list;
+ int i;
+
+ list.refs = NULL;
+ list.alloc = list.count = 0;
+ refs_for_each_tag_ref(get_main_ref_store(the_repository),
+ cgit_refs_cb, &list);
+ if (list.count == 0)
+ return;
+ qsort(list.refs, list.count, sizeof(*list.refs), cmp_tag_age);
+ if (!maxcount)
+ maxcount = list.count;
+ else if (maxcount > list.count)
+ maxcount = list.count;
+ print_tag_header();
+ for (i = 0; i < maxcount; i++)
+ print_tag(list.refs[i]);
+
+ if (maxcount < list.count)
+ print_refs_link("tags");
+
+ cgit_free_reflist_inner(&list);
+}
+
+void cgit_print_refs(void)
+{
+ cgit_print_layout_start();
+ html("<table class='list nowrap'>");
+
+ if (ctx.qry.path && starts_with(ctx.qry.path, "heads"))
+ cgit_print_branches(0);
+ else if (ctx.qry.path && starts_with(ctx.qry.path, "tags"))
+ cgit_print_tags(0);
+ else {
+ cgit_print_branches(0);
+ html("<tr class='nohover'><td colspan='5'>&nbsp;</td></tr>");
+ cgit_print_tags(0);
+ }
+ html("</table>");
+ cgit_print_layout_end();
+}
diff --git a/source/ui-refs.h b/source/ui-refs.h
new file mode 100644
index 0000000..1d4a54a
--- /dev/null
+++ b/source/ui-refs.h
@@ -0,0 +1,8 @@
+#ifndef UI_REFS_H
+#define UI_REFS_H
+
+extern void cgit_print_branches(int maxcount);
+extern void cgit_print_tags(int maxcount);
+extern void cgit_print_refs(void);
+
+#endif /* UI_REFS_H */
diff --git a/source/ui-repolist.c b/source/ui-repolist.c
new file mode 100644
index 0000000..1b224cf
--- /dev/null
+++ b/source/ui-repolist.c
@@ -0,0 +1,379 @@
+/* ui-repolist.c: functions for generating the repolist page
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-repolist.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static time_t read_agefile(const char *path)
+{
+ time_t result;
+ size_t size;
+ char *buf = NULL;
+ struct strbuf date_buf = STRBUF_INIT;
+
+ if (read_first_line(path, &buf, &size)) {
+ free(buf);
+ return 0;
+ }
+
+ if (parse_date(buf, &date_buf) == 0)
+ result = strtoul(date_buf.buf, NULL, 10);
+ else
+ result = 0;
+ free(buf);
+ strbuf_release(&date_buf);
+ return result;
+}
+
+static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime)
+{
+ struct strbuf path = STRBUF_INIT;
+ struct stat s;
+ struct cgit_repo *r = (struct cgit_repo *)repo;
+
+ if (repo->mtime != -1) {
+ *mtime = repo->mtime;
+ return 1;
+ }
+ strbuf_addf(&path, "%s/%s", repo->path, ctx.cfg.agefile);
+ if (stat(path.buf, &s) == 0) {
+ *mtime = read_agefile(path.buf);
+ if (*mtime) {
+ r->mtime = *mtime;
+ goto end;
+ }
+ }
+
+ strbuf_reset(&path);
+ strbuf_addf(&path, "%s/refs/heads/%s", repo->path,
+ repo->defbranch ? repo->defbranch : "master");
+ if (stat(path.buf, &s) == 0) {
+ *mtime = s.st_mtime;
+ r->mtime = *mtime;
+ goto end;
+ }
+
+ strbuf_reset(&path);
+ strbuf_addf(&path, "%s/%s", repo->path, "packed-refs");
+ if (stat(path.buf, &s) == 0) {
+ *mtime = s.st_mtime;
+ r->mtime = *mtime;
+ goto end;
+ }
+
+ *mtime = 0;
+ r->mtime = *mtime;
+end:
+ strbuf_release(&path);
+ return (r->mtime != 0);
+}
+
+static void print_modtime(struct cgit_repo *repo)
+{
+ time_t t;
+ if (get_repo_modtime(repo, &t))
+ cgit_print_age(t, 0, -1);
+}
+
+static int is_match(struct cgit_repo *repo)
+{
+ if (!ctx.qry.search)
+ return 1;
+ if (repo->url && strcasestr(repo->url, ctx.qry.search))
+ return 1;
+ if (repo->name && strcasestr(repo->name, ctx.qry.search))
+ return 1;
+ if (repo->desc && strcasestr(repo->desc, ctx.qry.search))
+ return 1;
+ if (repo->owner && strcasestr(repo->owner, ctx.qry.search))
+ return 1;
+ return 0;
+}
+
+static int is_in_url(struct cgit_repo *repo)
+{
+ if (!ctx.qry.url)
+ return 1;
+ if (repo->url && starts_with(repo->url, ctx.qry.url))
+ return 1;
+ return 0;
+}
+
+static int is_visible(struct cgit_repo *repo)
+{
+ if (repo->hide || repo->ignore)
+ return 0;
+ if (!(is_match(repo) && is_in_url(repo)))
+ return 0;
+ return 1;
+}
+
+static int any_repos_visible(void)
+{
+ int i;
+
+ for (i = 0; i < cgit_repolist.count; i++) {
+ if (is_visible(&cgit_repolist.repos[i]))
+ return 1;
+ }
+ return 0;
+}
+
+static void print_sort_header(const char *title, const char *sort)
+{
+ char *currenturl = cgit_currenturl();
+ html("<th class='left'><a href='");
+ html_attr(currenturl);
+ htmlf("?s=%s", sort);
+ if (ctx.qry.search) {
+ html("&amp;q=");
+ html_url_arg(ctx.qry.search);
+ }
+ htmlf("'>%s</a></th>", title);
+ free(currenturl);
+}
+
+static void print_header(void)
+{
+ html("<tr class='nohover'>");
+ print_sort_header("Name", "name");
+ print_sort_header("Description", "desc");
+ if (ctx.cfg.enable_index_owner)
+ print_sort_header("Owner", "owner");
+ print_sort_header("Idle", "idle");
+ if (ctx.cfg.enable_index_links)
+ html("<th class='left'>Links</th>");
+ html("</tr>\n");
+}
+
+
+static void print_pager(int items, int pagelen, char *search, char *sort)
+{
+ int i, ofs;
+ char *class = NULL;
+ html("<ul class='pager'>");
+ for (i = 0, ofs = 0; ofs < items; i++, ofs = i * pagelen) {
+ class = (ctx.qry.ofs == ofs) ? "current" : NULL;
+ html("<li>");
+ cgit_index_link(fmt("[%d]", i + 1), fmt("Page %d", i + 1),
+ class, search, sort, ofs, 0);
+ html("</li>");
+ }
+ html("</ul>");
+}
+
+static int cmp(const char *s1, const char *s2)
+{
+ if (s1 && s2) {
+ if (ctx.cfg.case_sensitive_sort)
+ return strcmp(s1, s2);
+ else
+ return strcasecmp(s1, s2);
+ }
+ if (s1 && !s2)
+ return -1;
+ if (s2 && !s1)
+ return 1;
+ return 0;
+}
+
+static int sort_name(const void *a, const void *b)
+{
+ const struct cgit_repo *r1 = a;
+ const struct cgit_repo *r2 = b;
+
+ return cmp(r1->name, r2->name);
+}
+
+static int sort_desc(const void *a, const void *b)
+{
+ const struct cgit_repo *r1 = a;
+ const struct cgit_repo *r2 = b;
+
+ return cmp(r1->desc, r2->desc);
+}
+
+static int sort_owner(const void *a, const void *b)
+{
+ const struct cgit_repo *r1 = a;
+ const struct cgit_repo *r2 = b;
+
+ return cmp(r1->owner, r2->owner);
+}
+
+static int sort_idle(const void *a, const void *b)
+{
+ const struct cgit_repo *r1 = a;
+ const struct cgit_repo *r2 = b;
+ time_t t1, t2;
+
+ t1 = t2 = 0;
+ get_repo_modtime(r1, &t1);
+ get_repo_modtime(r2, &t2);
+ return t2 - t1;
+}
+
+static int sort_section(const void *a, const void *b)
+{
+ const struct cgit_repo *r1 = a;
+ const struct cgit_repo *r2 = b;
+ int result;
+
+ result = cmp(r1->section, r2->section);
+ if (!result) {
+ if (!strcmp(ctx.cfg.repository_sort, "age"))
+ result = sort_idle(r1, r2);
+ if (!result)
+ result = cmp(r1->name, r2->name);
+ }
+ return result;
+}
+
+struct sortcolumn {
+ const char *name;
+ int (*fn)(const void *a, const void *b);
+};
+
+static const struct sortcolumn sortcolumn[] = {
+ {"section", sort_section},
+ {"name", sort_name},
+ {"desc", sort_desc},
+ {"owner", sort_owner},
+ {"idle", sort_idle},
+ {NULL, NULL}
+};
+
+static int sort_repolist(char *field)
+{
+ const struct sortcolumn *column;
+
+ for (column = &sortcolumn[0]; column->name; column++) {
+ if (strcmp(field, column->name))
+ continue;
+ qsort(cgit_repolist.repos, cgit_repolist.count,
+ sizeof(struct cgit_repo), column->fn);
+ return 1;
+ }
+ return 0;
+}
+
+
+void cgit_print_repolist(void)
+{
+ int i, columns = 3, hits = 0, header = 0;
+ char *last_section = NULL;
+ char *section;
+ char *repourl;
+ int sorted = 0;
+
+ if (!any_repos_visible()) {
+ cgit_print_error_page(404, "Not found", "No repositories found");
+ return;
+ }
+
+ if (ctx.cfg.enable_index_links)
+ ++columns;
+ if (ctx.cfg.enable_index_owner)
+ ++columns;
+
+ ctx.page.title = ctx.cfg.root_title;
+ cgit_print_http_headers();
+ cgit_print_docstart();
+ cgit_print_pageheader();
+
+ if (ctx.qry.sort)
+ sorted = sort_repolist(ctx.qry.sort);
+ else if (ctx.cfg.section_sort)
+ sort_repolist("section");
+
+ html("<table summary='repository list' class='list nowrap'>");
+ for (i = 0; i < cgit_repolist.count; i++) {
+ ctx.repo = &cgit_repolist.repos[i];
+ if (!is_visible(ctx.repo))
+ continue;
+ hits++;
+ if (hits <= ctx.qry.ofs)
+ continue;
+ if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count)
+ continue;
+ if (!header++)
+ print_header();
+ section = ctx.repo->section;
+ if (section && !strcmp(section, ""))
+ section = NULL;
+ if (!sorted &&
+ ((last_section == NULL && section != NULL) ||
+ (last_section != NULL && section == NULL) ||
+ (last_section != NULL && section != NULL &&
+ strcmp(section, last_section)))) {
+ htmlf("<tr class='nohover-highlight'><td colspan='%d' class='reposection'>",
+ columns);
+ html_txt(section);
+ html("</td></tr>");
+ last_section = section;
+ }
+ htmlf("<tr><td class='%s'>",
+ !sorted && section ? "sublevel-repo" : "toplevel-repo");
+ cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
+ html("</td><td>");
+ repourl = cgit_repourl(ctx.repo->url);
+ html_link_open(repourl, NULL, NULL);
+ free(repourl);
+ if (html_ntxt(ctx.repo->desc, ctx.cfg.max_repodesc_len) < 0)
+ html("...");
+ html_link_close();
+ html("</td><td>");
+ if (ctx.cfg.enable_index_owner) {
+ if (ctx.repo->owner_filter) {
+ cgit_open_filter(ctx.repo->owner_filter);
+ html_txt(ctx.repo->owner);
+ cgit_close_filter(ctx.repo->owner_filter);
+ } else {
+ char *currenturl = cgit_currenturl();
+ html("<a href='");
+ html_attr(currenturl);
+ html("?q=");
+ html_url_arg(ctx.repo->owner);
+ html("'>");
+ html_txt(ctx.repo->owner);
+ html("</a>");
+ free(currenturl);
+ }
+ html("</td><td>");
+ }
+ print_modtime(ctx.repo);
+ html("</td>");
+ if (ctx.cfg.enable_index_links) {
+ html("<td>");
+ cgit_summary_link("summary", NULL, "button", NULL);
+ cgit_log_link("log", NULL, "button", NULL, NULL, NULL,
+ 0, NULL, NULL, ctx.qry.showmsg, 0);
+ cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL);
+ html("</td>");
+ }
+ html("</tr>\n");
+ }
+ html("</table>");
+ if (hits > ctx.cfg.max_repo_count)
+ print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search, ctx.qry.sort);
+ cgit_print_docend();
+}
+
+void cgit_print_site_readme(void)
+{
+ cgit_print_layout_start();
+ if (!ctx.cfg.root_readme)
+ goto done;
+ cgit_open_filter(ctx.cfg.about_filter, ctx.cfg.root_readme);
+ html_include(ctx.cfg.root_readme);
+ cgit_close_filter(ctx.cfg.about_filter);
+done:
+ cgit_print_layout_end();
+}
diff --git a/source/ui-repolist.h b/source/ui-repolist.h
new file mode 100644
index 0000000..1b6b322
--- /dev/null
+++ b/source/ui-repolist.h
@@ -0,0 +1,7 @@
+#ifndef UI_REPOLIST_H
+#define UI_REPOLIST_H
+
+extern void cgit_print_repolist(void);
+extern void cgit_print_site_readme(void);
+
+#endif /* UI_REPOLIST_H */
diff --git a/source/ui-shared.c b/source/ui-shared.c
new file mode 100644
index 0000000..df52a9b
--- /dev/null
+++ 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
new file mode 100644
index 0000000..2a3a7f5
--- /dev/null
+++ b/source/ui-shared.h
@@ -0,0 +1,88 @@
+#ifndef UI_SHARED_H
+#define UI_SHARED_H
+
+extern const char *cgit_httpscheme(void);
+extern char *cgit_hosturl(void);
+extern const char *cgit_rooturl(void);
+extern char *cgit_currenturl(void);
+extern char *cgit_currentfullurl(void);
+extern const char *cgit_loginurl(void);
+extern char *cgit_repourl(const char *reponame);
+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);
+
+extern void cgit_add_clone_urls(void (*fn)(const char *));
+
+extern void cgit_index_link(const char *name, const char *title,
+ const char *class, const char *pattern, const char *sort, int ofs, int always_root);
+extern void cgit_summary_link(const char *name, const char *title,
+ const char *class, const char *head);
+extern void cgit_tag_link(const char *name, const char *title,
+ const char *class, const char *tag);
+extern void cgit_tree_link(const char *name, const char *title,
+ const char *class, const char *head,
+ const char *rev, const char *path);
+extern void cgit_plain_link(const char *name, const char *title,
+ const char *class, const char *head,
+ const char *rev, const char *path);
+extern void cgit_blame_link(const char *name, const char *title,
+ const char *class, const char *head,
+ const char *rev, const char *path);
+extern void cgit_log_link(const char *name, const char *title,
+ const char *class, const char *head, const char *rev,
+ const char *path, int ofs, const char *grep,
+ const char *pattern, int showmsg, int follow);
+extern void cgit_commit_link(const char *name, const char *title,
+ const char *class, const char *head,
+ const char *rev, const char *path);
+extern void cgit_patch_link(const char *name, const char *title,
+ const char *class, const char *head,
+ const char *rev, const char *path);
+extern void cgit_refs_link(const char *name, const char *title,
+ const char *class, const char *head,
+ const char *rev, const char *path);
+extern void cgit_snapshot_link(const char *name, const char *title,
+ const char *class, const char *head,
+ const char *rev, const char *archivename);
+extern void cgit_diff_link(const char *name, const char *title,
+ const char *class, const char *head,
+ const char *new_rev, const char *old_rev,
+ const char *path);
+extern void cgit_stats_link(const char *name, const char *title,
+ const char *class, const char *head,
+ const char *path);
+extern void cgit_object_link(struct object *obj);
+
+extern void cgit_submodule_link(const char *class, char *path,
+ const char *rev);
+
+extern void cgit_print_layout_start(void);
+extern void cgit_print_layout_end(void);
+
+__attribute__((format (printf,1,2)))
+extern void cgit_print_error(const char *fmt, ...);
+__attribute__((format (printf,1,0)))
+extern void cgit_vprint_error(const char *fmt, va_list ap);
+extern const struct date_mode cgit_date_mode(enum date_mode_type type);
+extern void cgit_print_age(time_t t, int tz, time_t max_relative);
+extern void cgit_print_http_headers(void);
+extern void cgit_redirect(const char *url, 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);
+extern void cgit_print_pageheader(void);
+extern void cgit_print_filemode(unsigned short mode);
+extern void cgit_compose_snapshot_prefix(struct strbuf *filename,
+ const char *base, const char *ref);
+extern void cgit_print_snapshot_links(const struct cgit_repo *repo,
+ const char *ref, const char *separator);
+extern const char *cgit_snapshot_prefix(const struct cgit_repo *repo);
+extern void cgit_add_hidden_formfields(int incl_head, int incl_search,
+ const char *page);
+
+extern void cgit_set_title_from_path(const char *path);
+#endif /* UI_SHARED_H */
diff --git a/source/ui-snapshot.c b/source/ui-snapshot.c
new file mode 100644
index 0000000..d157222
--- /dev/null
+++ b/source/ui-snapshot.c
@@ -0,0 +1,318 @@
+/* ui-snapshot.c: generate snapshot of a commit
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-snapshot.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static int write_archive_type(const char *format, const char *hex, const char *prefix)
+{
+ struct strvec argv = STRVEC_INIT;
+ const char **nargv;
+ int result;
+ strvec_push(&argv, "snapshot");
+ strvec_push(&argv, format);
+ if (prefix) {
+ struct strbuf buf = STRBUF_INIT;
+ strbuf_addstr(&buf, prefix);
+ strbuf_addch(&buf, '/');
+ strvec_push(&argv, "--prefix");
+ strvec_push(&argv, buf.buf);
+ strbuf_release(&buf);
+ }
+ strvec_push(&argv, hex);
+ /*
+ * Now we need to copy the pointers to arguments into a new
+ * structure because write_archive will rearrange its arguments
+ * which may result in duplicated/missing entries causing leaks
+ * or double-frees in strvec_clear.
+ */
+ nargv = xmalloc(sizeof(char *) * (argv.nr + 1));
+ /* strvec guarantees a trailing NULL entry. */
+ memcpy(nargv, argv.v, sizeof(char *) * (argv.nr + 1));
+
+ result = write_archive(argv.nr, nargv, NULL, the_repository, NULL, 0);
+ strvec_clear(&argv);
+ free(nargv);
+ return result;
+}
+
+static int write_tar_archive(const char *hex, const char *prefix)
+{
+ return write_archive_type("--format=tar", hex, prefix);
+}
+
+static int write_zip_archive(const char *hex, const char *prefix)
+{
+ return write_archive_type("--format=zip", hex, prefix);
+}
+
+static int write_compressed_tar_archive(const char *hex,
+ const char *prefix,
+ char *filter_argv[])
+{
+ int rv;
+ struct cgit_exec_filter f;
+ cgit_exec_filter_init(&f, filter_argv[0], filter_argv);
+
+ cgit_open_filter(&f.base);
+ rv = write_tar_archive(hex, prefix);
+ cgit_close_filter(&f.base);
+ return rv;
+}
+
+static int write_tar_gzip_archive(const char *hex, const char *prefix)
+{
+ 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)
+{
+ char *argv[] = { "bzip2", NULL };
+ return write_compressed_tar_archive(hex, prefix, argv);
+}
+
+static int write_tar_lzip_archive(const char *hex, const char *prefix)
+{
+ char *argv[] = { "lzip", NULL };
+ return write_compressed_tar_archive(hex, prefix, argv);
+}
+
+static int write_tar_xz_archive(const char *hex, const char *prefix)
+{
+ char *argv[] = { "xz", NULL };
+ return write_compressed_tar_archive(hex, prefix, argv);
+}
+
+static int write_tar_zstd_archive(const char *hex, const char *prefix)
+{
+ char *argv[] = { "zstd", "-T0", NULL };
+ return write_compressed_tar_archive(hex, prefix, argv);
+}
+
+const struct cgit_snapshot_format cgit_snapshot_formats[] = {
+ /* .tar must remain the 0 index */
+ { ".tar", "application/x-tar", write_tar_archive },
+ { ".tar.gz", "application/x-gzip", write_tar_gzip_archive },
+ { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive },
+ { ".tar.lz", "application/x-lzip", write_tar_lzip_archive },
+ { ".tar.xz", "application/x-xz", write_tar_xz_archive },
+ { ".tar.zst", "application/x-zstd", write_tar_zstd_archive },
+ { ".zip", "application/x-zip", write_zip_archive },
+ { NULL }
+};
+
+static struct notes_tree snapshot_sig_notes[ARRAY_SIZE(cgit_snapshot_formats)];
+
+const struct object_id *cgit_snapshot_get_sig(const char *ref,
+ const struct cgit_snapshot_format *f)
+{
+ struct notes_tree *tree;
+ struct object_id oid;
+
+ if (repo_get_oid(the_repository, ref, &oid))
+ return NULL;
+
+ tree = &snapshot_sig_notes[f - &cgit_snapshot_formats[0]];
+ if (!tree->initialized) {
+ struct strbuf notes_ref = STRBUF_INIT;
+
+ strbuf_addf(&notes_ref, "refs/notes/signatures/%s",
+ f->suffix + 1);
+
+ init_notes(tree, notes_ref.buf, combine_notes_ignore, 0);
+ strbuf_release(&notes_ref);
+ }
+
+ return get_note(tree, &oid);
+}
+
+static const struct cgit_snapshot_format *get_format(const char *filename)
+{
+ const struct cgit_snapshot_format *fmt;
+
+ for (fmt = cgit_snapshot_formats; fmt->suffix; fmt++) {
+ if (ends_with(filename, fmt->suffix))
+ return fmt;
+ }
+ return NULL;
+}
+
+const unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f)
+{
+ return BIT(f - &cgit_snapshot_formats[0]);
+}
+
+static int make_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(400, "Bad request",
+ "Not a commit reference: %s", hex);
+ return 1;
+ }
+ ctx.page.etag = oid_to_hex(&oid);
+ ctx.page.mimetype = xstrdup(format->mimetype);
+ ctx.page.filename = xstrdup(filename);
+ cgit_print_http_headers();
+ init_archivers();
+ format->write_func(hex, prefix);
+ return 0;
+}
+
+static int write_sig(const struct cgit_snapshot_format *format,
+ const char *hex, const char *archive,
+ const char *filename)
+{
+ const struct object_id *note = cgit_snapshot_get_sig(hex, format);
+ enum object_type type;
+ unsigned long size;
+ char *buf;
+
+ if (!note) {
+ cgit_print_error_page(404, "Not found",
+ "No signature for %s", archive);
+ return 0;
+ }
+
+ buf = odb_read_object(the_repository->objects, note, &type, &size);
+ if (!buf) {
+ cgit_print_error_page(404, "Not found", "Not found");
+ return 0;
+ }
+
+ html("X-Content-Type-Options: nosniff\n");
+ html("Content-Security-Policy: default-src 'none'\n");
+ ctx.page.etag = oid_to_hex(note);
+ ctx.page.mimetype = xstrdup("application/pgp-signature");
+ ctx.page.filename = xstrdup(filename);
+ cgit_print_http_headers();
+
+ html_raw(buf, size);
+ free(buf);
+ return 0;
+}
+
+/* Try to guess the requested revision from the requested snapshot name.
+ * First the format extension is stripped, e.g. "cgit-0.7.2.tar.gz" become
+ * "cgit-0.7.2". If this is a valid commit object name we've got a winner.
+ * Otherwise, if the snapshot name has a prefix matching the result from
+ * repo_basename(), we strip the basename and any following '-' and '_'
+ * characters ("cgit-0.7.2" -> "0.7.2") and check the resulting name once
+ * more. If this still isn't a valid commit object name, we check if pre-
+ * pending a 'v' or a 'V' to the remaining snapshot name ("0.7.2" ->
+ * "v0.7.2") gives us something valid.
+ */
+static const char *get_ref_from_filename(const struct cgit_repo *repo,
+ const char *filename,
+ const struct cgit_snapshot_format *format)
+{
+ const char *reponame;
+ struct object_id oid;
+ struct strbuf snapshot = STRBUF_INIT;
+ int result = 1;
+
+ strbuf_addstr(&snapshot, filename);
+ strbuf_setlen(&snapshot, snapshot.len - strlen(format->suffix));
+
+ if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
+ goto out;
+
+ reponame = cgit_snapshot_prefix(repo);
+ if (starts_with(snapshot.buf, reponame)) {
+ const char *new_start = snapshot.buf;
+ new_start += strlen(reponame);
+ while (new_start && (*new_start == '-' || *new_start == '_'))
+ new_start++;
+ strbuf_splice(&snapshot, 0, new_start - snapshot.buf, "", 0);
+ }
+
+ if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
+ goto out;
+
+ strbuf_insert(&snapshot, 0, "v", 1);
+ if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
+ goto out;
+
+ strbuf_splice(&snapshot, 0, 1, "V", 1);
+ if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
+ goto out;
+
+ result = 0;
+ strbuf_release(&snapshot);
+
+out:
+ return result ? strbuf_detach(&snapshot, NULL) : NULL;
+}
+
+void cgit_print_snapshot(const char *head, const char *hex,
+ const char *filename, int dwim)
+{
+ const struct cgit_snapshot_format* f;
+ const char *sig_filename = NULL;
+ char *adj_filename = NULL;
+ char *prefix = NULL;
+
+ if (!filename) {
+ cgit_print_error_page(400, "Bad request",
+ "No snapshot name specified");
+ return;
+ }
+
+ if (ends_with(filename, ".asc")) {
+ sig_filename = filename;
+
+ /* Strip ".asc" from filename for common format processing */
+ adj_filename = xstrdup(filename);
+ adj_filename[strlen(adj_filename) - 4] = '\0';
+ filename = adj_filename;
+ }
+
+ f = get_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;
+ }
+
+ if (!hex && dwim) {
+ hex = get_ref_from_filename(ctx.repo, filename, f);
+ if (hex == NULL) {
+ cgit_print_error_page(404, "Not found", "Not found");
+ return;
+ }
+ prefix = xstrdup(filename);
+ prefix[strlen(filename) - strlen(f->suffix)] = '\0';
+ }
+
+ if (!hex)
+ hex = head;
+
+ if (!prefix)
+ prefix = xstrdup(cgit_snapshot_prefix(ctx.repo));
+
+ if (sig_filename)
+ write_sig(f, hex, filename, sig_filename);
+ else
+ make_snapshot(f, hex, prefix, filename);
+
+ free(prefix);
+ free(adj_filename);
+}
diff --git a/source/ui-snapshot.h b/source/ui-snapshot.h
new file mode 100644
index 0000000..a8deec3
--- /dev/null
+++ b/source/ui-snapshot.h
@@ -0,0 +1,7 @@
+#ifndef UI_SNAPSHOT_H
+#define UI_SNAPSHOT_H
+
+extern void cgit_print_snapshot(const char *head, const char *hex,
+ const char *filename, int dwim);
+
+#endif /* UI_SNAPSHOT_H */
diff --git a/source/ui-ssdiff.c b/source/ui-ssdiff.c
new file mode 100644
index 0000000..af8bc9e
--- /dev/null
+++ b/source/ui-ssdiff.c
@@ -0,0 +1,420 @@
+#include "cgit.h"
+#include "ui-ssdiff.h"
+#include "html.h"
+#include "ui-shared.h"
+#include "ui-diff.h"
+
+extern int use_ssdiff;
+
+static int current_old_line, current_new_line;
+static int **L = NULL;
+
+struct deferred_lines {
+ int line_no;
+ char *line;
+ struct deferred_lines *next;
+};
+
+static struct deferred_lines *deferred_old, *deferred_old_last;
+static struct deferred_lines *deferred_new, *deferred_new_last;
+
+static void create_or_reset_lcs_table(void)
+{
+ int i;
+
+ if (L != NULL) {
+ memset(*L, 0, sizeof(int) * MAX_SSDIFF_SIZE);
+ return;
+ }
+
+ // xcalloc will die if we ran out of memory;
+ // not very helpful for debugging
+ L = (int**)xcalloc(MAX_SSDIFF_M, sizeof(int *));
+ *L = (int*)xcalloc(MAX_SSDIFF_SIZE, sizeof(int));
+
+ for (i = 1; i < MAX_SSDIFF_M; i++) {
+ L[i] = *L + i * MAX_SSDIFF_N;
+ }
+}
+
+static char *longest_common_subsequence(char *A, char *B)
+{
+ int i, j, ri;
+ int m = strlen(A);
+ int n = strlen(B);
+ int tmp1, tmp2;
+ int lcs_length;
+ char *result;
+
+ // We bail if the lines are too long
+ if (m >= MAX_SSDIFF_M || n >= MAX_SSDIFF_N)
+ return NULL;
+
+ create_or_reset_lcs_table();
+
+ for (i = m; i >= 0; i--) {
+ for (j = n; j >= 0; j--) {
+ if (A[i] == '\0' || B[j] == '\0') {
+ L[i][j] = 0;
+ } else if (A[i] == B[j]) {
+ L[i][j] = 1 + L[i + 1][j + 1];
+ } else {
+ tmp1 = L[i + 1][j];
+ tmp2 = L[i][j + 1];
+ L[i][j] = (tmp1 > tmp2 ? tmp1 : tmp2);
+ }
+ }
+ }
+
+ lcs_length = L[0][0];
+ result = xmalloc(lcs_length + 2);
+ memset(result, 0, sizeof(*result) * (lcs_length + 2));
+
+ ri = 0;
+ i = 0;
+ j = 0;
+ while (i < m && j < n) {
+ if (A[i] == B[j]) {
+ result[ri] = A[i];
+ ri += 1;
+ i += 1;
+ j += 1;
+ } else if (L[i + 1][j] >= L[i][j + 1]) {
+ i += 1;
+ } else {
+ j += 1;
+ }
+ }
+
+ return result;
+}
+
+static int line_from_hunk(char *line, char type)
+{
+ char *buf1, *buf2;
+ int len, res;
+
+ buf1 = strchr(line, type);
+ if (buf1 == NULL)
+ return 0;
+ buf1 += 1;
+ buf2 = strchr(buf1, ',');
+ if (buf2 == NULL)
+ return 0;
+ len = buf2 - buf1;
+ buf2 = xmalloc(len + 1);
+ strlcpy(buf2, buf1, len + 1);
+ res = atoi(buf2);
+ free(buf2);
+ return res;
+}
+
+static char *replace_tabs(char *line)
+{
+ char *prev_buf = line;
+ char *cur_buf;
+ size_t linelen = strlen(line);
+ int n_tabs = 0;
+ int i;
+ char *result;
+ size_t result_len;
+
+ if (linelen == 0) {
+ result = xmalloc(1);
+ result[0] = '\0';
+ return result;
+ }
+
+ for (i = 0; i < linelen; i++) {
+ if (line[i] == '\t')
+ n_tabs += 1;
+ }
+ result_len = linelen + n_tabs * 8;
+ result = xmalloc(result_len + 1);
+ result[0] = '\0';
+
+ for (;;) {
+ cur_buf = strchr(prev_buf, '\t');
+ if (!cur_buf) {
+ linelen = strlen(result);
+ strlcpy(&result[linelen], prev_buf, result_len - linelen + 1);
+ break;
+ } else {
+ linelen = strlen(result);
+ strlcpy(&result[linelen], prev_buf, cur_buf - prev_buf + 1);
+ linelen = strlen(result);
+ memset(&result[linelen], ' ', 8 - (linelen % 8));
+ result[linelen + 8 - (linelen % 8)] = '\0';
+ }
+ prev_buf = cur_buf + 1;
+ }
+ return result;
+}
+
+static int calc_deferred_lines(struct deferred_lines *start)
+{
+ struct deferred_lines *item = start;
+ int result = 0;
+ while (item) {
+ result += 1;
+ item = item->next;
+ }
+ return result;
+}
+
+static void deferred_old_add(char *line, int line_no)
+{
+ struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines));
+ item->line = xstrdup(line);
+ item->line_no = line_no;
+ item->next = NULL;
+ if (deferred_old) {
+ deferred_old_last->next = item;
+ deferred_old_last = item;
+ } else {
+ deferred_old = deferred_old_last = item;
+ }
+}
+
+static void deferred_new_add(char *line, int line_no)
+{
+ struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines));
+ item->line = xstrdup(line);
+ item->line_no = line_no;
+ item->next = NULL;
+ if (deferred_new) {
+ deferred_new_last->next = item;
+ deferred_new_last = item;
+ } else {
+ deferred_new = deferred_new_last = item;
+ }
+}
+
+static void print_part_with_lcs(char *class, char *line, char *lcs)
+{
+ int line_len = strlen(line);
+ int i, j;
+ char c[2] = " ";
+ int same = 1;
+
+ j = 0;
+ for (i = 0; i < line_len; i++) {
+ c[0] = line[i];
+ if (same) {
+ if (line[i] == lcs[j])
+ j += 1;
+ else {
+ same = 0;
+ htmlf("<span class='%s'>", class);
+ }
+ } else if (line[i] == lcs[j]) {
+ same = 1;
+ html("</span>");
+ j += 1;
+ }
+ html_txt(c);
+ }
+ if (!same)
+ html("</span>");
+}
+
+static void print_ssdiff_line(char *class,
+ int old_line_no,
+ char *old_line,
+ int new_line_no,
+ char *new_line, int individual_chars)
+{
+ char *lcs = NULL;
+
+ if (old_line)
+ old_line = replace_tabs(old_line + 1);
+ if (new_line)
+ new_line = replace_tabs(new_line + 1);
+ if (individual_chars && old_line && new_line)
+ lcs = longest_common_subsequence(old_line, new_line);
+ html("<tr>\n");
+ if (old_line_no > 0) {
+ struct diff_filespec *old_file = cgit_get_current_old_file();
+ char *lineno_str = fmt("n%d", old_line_no);
+ char *id_str = fmt("id=%s#%s", is_null_oid(&old_file->oid)?"HEAD":oid_to_hex(old_rev_oid), lineno_str);
+ char *fileurl = cgit_fileurl(ctx.repo->url, "tree", old_file->path, id_str);
+ html("<td class='lineno'><a href='");
+ html(fileurl);
+ htmlf("'>%s</a>", lineno_str + 1);
+ html("</td>");
+ htmlf("<td class='%s'>", class);
+ free(fileurl);
+ } 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_part_with_lcs("del", old_line, lcs);
+ else
+ html_txt(old_line);
+ }
+
+ html("</td>\n");
+ if (new_line_no > 0) {
+ struct diff_filespec *new_file = cgit_get_current_new_file();
+ char *lineno_str = fmt("n%d", new_line_no);
+ char *id_str = fmt("id=%s#%s", is_null_oid(&new_file->oid)?"HEAD":oid_to_hex(new_rev_oid), lineno_str);
+ char *fileurl = cgit_fileurl(ctx.repo->url, "tree", new_file->path, id_str);
+ html("<td class='lineno'><a href='");
+ html(fileurl);
+ htmlf("'>%s</a>", lineno_str + 1);
+ html("</td>");
+ htmlf("<td class='%s'>", class);
+ free(fileurl);
+ } 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_part_with_lcs("add", new_line, lcs);
+ else
+ html_txt(new_line);
+ }
+
+ html("</td></tr>");
+ if (lcs)
+ free(lcs);
+ if (new_line)
+ free(new_line);
+ if (old_line)
+ free(old_line);
+}
+
+static void print_deferred_old_lines(void)
+{
+ struct deferred_lines *iter_old, *tmp;
+ iter_old = deferred_old;
+ while (iter_old) {
+ print_ssdiff_line("del", iter_old->line_no,
+ iter_old->line, -1, NULL, 0);
+ tmp = iter_old->next;
+ free(iter_old);
+ iter_old = tmp;
+ }
+}
+
+static void print_deferred_new_lines(void)
+{
+ struct deferred_lines *iter_new, *tmp;
+ iter_new = deferred_new;
+ while (iter_new) {
+ print_ssdiff_line("add", -1, NULL,
+ iter_new->line_no, iter_new->line, 0);
+ tmp = iter_new->next;
+ free(iter_new);
+ iter_new = tmp;
+ }
+}
+
+static void print_deferred_changed_lines(void)
+{
+ struct deferred_lines *iter_old, *iter_new, *tmp;
+ int n_old_lines = calc_deferred_lines(deferred_old);
+ int n_new_lines = calc_deferred_lines(deferred_new);
+ int individual_chars = (n_old_lines == n_new_lines ? 1 : 0);
+
+ iter_old = deferred_old;
+ iter_new = deferred_new;
+ while (iter_old || iter_new) {
+ if (iter_old && iter_new)
+ print_ssdiff_line("changed", iter_old->line_no,
+ iter_old->line,
+ iter_new->line_no, iter_new->line,
+ individual_chars);
+ else if (iter_old)
+ print_ssdiff_line("changed", iter_old->line_no,
+ iter_old->line, -1, NULL, 0);
+ else if (iter_new)
+ print_ssdiff_line("changed", -1, NULL,
+ iter_new->line_no, iter_new->line, 0);
+ if (iter_old) {
+ tmp = iter_old->next;
+ free(iter_old);
+ iter_old = tmp;
+ }
+
+ if (iter_new) {
+ tmp = iter_new->next;
+ free(iter_new);
+ iter_new = tmp;
+ }
+ }
+}
+
+void cgit_ssdiff_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();
+ deferred_old = deferred_old_last = NULL;
+ deferred_new = deferred_new_last = NULL;
+}
+
+/*
+ * print a single line returned from xdiff
+ */
+void cgit_ssdiff_line_cb(char *line, int len)
+{
+ char c = line[len - 1];
+ line[len - 1] = '\0';
+ if (line[0] == '@') {
+ current_old_line = line_from_hunk(line, '-');
+ current_new_line = line_from_hunk(line, '+');
+ }
+
+ if (line[0] == ' ') {
+ if (deferred_old || deferred_new)
+ cgit_ssdiff_print_deferred_lines();
+ print_ssdiff_line("ctx", current_old_line, line,
+ current_new_line, line, 0);
+ current_old_line += 1;
+ current_new_line += 1;
+ } else if (line[0] == '+') {
+ deferred_new_add(line, current_new_line);
+ current_new_line += 1;
+ } else if (line[0] == '-') {
+ deferred_old_add(line, current_old_line);
+ current_old_line += 1;
+ } else if (line[0] == '@') {
+ html("<tr><td colspan='4' class='hunk'>");
+ html_txt(line);
+ html("</td></tr>");
+ } else {
+ html("<tr><td colspan='4' class='ctx'>");
+ html_txt(line);
+ html("</td></tr>");
+ }
+ line[len - 1] = c;
+}
+
+void cgit_ssdiff_header_begin(void)
+{
+ current_old_line = -1;
+ current_new_line = -1;
+ html("<tr><td class='space' colspan='4'><div></div></td></tr>");
+ html("<tr><td class='head' colspan='4'>");
+}
+
+void cgit_ssdiff_header_end(void)
+{
+ html("</td></tr>");
+}
+
+void cgit_ssdiff_footer(void)
+{
+ if (deferred_old || deferred_new)
+ cgit_ssdiff_print_deferred_lines();
+ html("<tr><td class='foot' colspan='4'></td></tr>");
+}
diff --git a/source/ui-ssdiff.h b/source/ui-ssdiff.h
new file mode 100644
index 0000000..11f2714
--- /dev/null
+++ b/source/ui-ssdiff.h
@@ -0,0 +1,25 @@
+#ifndef UI_SSDIFF_H
+#define UI_SSDIFF_H
+
+/*
+ * ssdiff line limits
+ */
+#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_print_deferred_lines(void);
+
+extern void cgit_ssdiff_line_cb(char *line, int len);
+
+extern void cgit_ssdiff_header_begin(void);
+extern void cgit_ssdiff_header_end(void);
+
+extern void cgit_ssdiff_footer(void);
+
+#endif /* UI_SSDIFF_H */
diff --git a/source/ui-stats.c b/source/ui-stats.c
new file mode 100644
index 0000000..02c60ef
--- /dev/null
+++ b/source/ui-stats.c
@@ -0,0 +1,435 @@
+/* ui-stats.c: generate stats view
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-stats.h"
+#include "html.h"
+#include "ui-shared.h"
+
+struct authorstat {
+ long total;
+ struct string_list list;
+};
+
+#define DAY_SECS (60 * 60 * 24)
+#define WEEK_SECS (DAY_SECS * 7)
+
+static void trunc_week(struct tm *tm)
+{
+ time_t t = timegm(tm);
+ t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
+ gmtime_r(&t, tm);
+}
+
+static void dec_week(struct tm *tm)
+{
+ time_t t = timegm(tm);
+ t -= WEEK_SECS;
+ gmtime_r(&t, tm);
+}
+
+static void inc_week(struct tm *tm)
+{
+ time_t t = timegm(tm);
+ t += WEEK_SECS;
+ gmtime_r(&t, tm);
+}
+
+static char *pretty_week(struct tm *tm)
+{
+ static char buf[10];
+
+ strftime(buf, sizeof(buf), "W%V %G", tm);
+ return buf;
+}
+
+static void trunc_month(struct tm *tm)
+{
+ tm->tm_mday = 1;
+}
+
+static void dec_month(struct tm *tm)
+{
+ tm->tm_mon--;
+ 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_year++;
+ tm->tm_mon = 0;
+ }
+}
+
+static char *pretty_month(struct tm *tm)
+{
+ static const char *months[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+ };
+ return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
+}
+
+static void trunc_quarter(struct tm *tm)
+{
+ trunc_month(tm);
+ while (tm->tm_mon % 3 != 0)
+ dec_month(tm);
+}
+
+static void dec_quarter(struct tm *tm)
+{
+ 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);
+}
+
+static char *pretty_quarter(struct tm *tm)
+{
+ return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
+}
+
+static void trunc_year(struct tm *tm)
+{
+ trunc_month(tm);
+ tm->tm_mon = 0;
+}
+
+static void dec_year(struct tm *tm)
+{
+ tm->tm_year--;
+}
+
+static void inc_year(struct tm *tm)
+{
+ tm->tm_year++;
+}
+
+static char *pretty_year(struct tm *tm)
+{
+ return fmt("%d", tm->tm_year + 1900);
+}
+
+static const struct cgit_period periods[] = {
+ {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
+ {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
+ {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
+ {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
+};
+
+/* Given a period code or name, return a period index (1, 2, 3 or 4)
+ * and update the period pointer to the correcsponding struct.
+ * If no matching code is found, return 0.
+ */
+int cgit_find_stats_period(const char *expr, const struct cgit_period **period)
+{
+ int i;
+ char code = '\0';
+
+ if (!expr)
+ return 0;
+
+ if (strlen(expr) == 1)
+ code = expr[0];
+
+ for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
+ if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
+ if (period)
+ *period = &periods[i];
+ return i + 1;
+ }
+ return 0;
+}
+
+const char *cgit_find_stats_periodname(int idx)
+{
+ if (idx > 0 && idx < 4)
+ return periods[idx - 1].name;
+ else
+ return "";
+}
+
+static void add_commit(struct string_list *authors, struct commit *commit,
+ const struct cgit_period *period)
+{
+ struct commitinfo *info;
+ struct string_list_item *author, *item;
+ struct authorstat *authorstat;
+ struct string_list *items;
+ char *tmp;
+ struct tm date;
+ time_t t;
+ uintptr_t *counter;
+
+ info = cgit_parse_commit(commit);
+ tmp = xstrdup(info->author);
+ author = string_list_insert(authors, tmp);
+ if (!author->util)
+ author->util = xcalloc(1, sizeof(struct authorstat));
+ else
+ free(tmp);
+ authorstat = author->util;
+ items = &authorstat->list;
+ t = info->committer_date;
+ gmtime_r(&t, &date);
+ period->trunc(&date);
+ tmp = xstrdup(period->pretty(&date));
+ item = string_list_insert(items, tmp);
+ counter = (uintptr_t *)&item->util;
+ if (*counter)
+ free(tmp);
+ (*counter)++;
+
+ authorstat->total++;
+ cgit_free_commitinfo(info);
+}
+
+static int cmp_total_commits(const void *a1, const void *a2)
+{
+ const struct string_list_item *i1 = a1;
+ const struct string_list_item *i2 = a2;
+ const struct authorstat *auth1 = i1->util;
+ const struct authorstat *auth2 = i2->util;
+
+ return auth2->total - auth1->total;
+}
+
+/* Walk the commit DAG and collect number of commits per author per
+ * timeperiod into a nested string_list collection.
+ */
+static struct string_list collect_stats(const struct cgit_period *period)
+{
+ struct string_list authors;
+ struct rev_info rev;
+ struct commit *commit;
+ const char *argv[] = {NULL, ctx.qry.head, NULL, NULL, NULL, NULL};
+ int argc = 3;
+ time_t now;
+ long i;
+ struct tm tm;
+ char tmp[11];
+
+ time(&now);
+ gmtime_r(&now, &tm);
+ period->trunc(&tm);
+ for (i = 1; i < period->count; i++)
+ period->dec(&tm);
+ strftime(tmp, sizeof(tmp), "%Y-%m-%d", &tm);
+ argv[2] = xstrdup(fmt("--since=%s", tmp));
+ if (ctx.qry.path) {
+ argv[3] = "--";
+ argv[4] = ctx.qry.path;
+ argc += 2;
+ }
+ 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);
+ prepare_revision_walk(&rev);
+ memset(&authors, 0, sizeof(authors));
+ while ((commit = get_revision(&rev)) != NULL) {
+ add_commit(&authors, commit, period);
+ release_commit_memory(the_repository->parsed_objects, commit);
+ commit->parents = NULL;
+ }
+ return authors;
+}
+
+static void print_combined_authorrow(struct string_list *authors, int from,
+ int to, const char *name,
+ const char *leftclass,
+ const char *centerclass,
+ const char *rightclass,
+ const struct cgit_period *period)
+{
+ struct string_list_item *author;
+ struct authorstat *authorstat;
+ struct string_list *items;
+ struct string_list_item *date;
+ time_t now;
+ long i, j, total, subtotal;
+ struct tm tm;
+ char *tmp;
+
+ time(&now);
+ gmtime_r(&now, &tm);
+ period->trunc(&tm);
+ for (i = 1; i < period->count; i++)
+ period->dec(&tm);
+
+ total = 0;
+ htmlf("<tr><td class='%s'>%s</td>", leftclass,
+ fmt(name, to - from + 1));
+ for (j = 0; j < period->count; j++) {
+ tmp = period->pretty(&tm);
+ period->inc(&tm);
+ subtotal = 0;
+ for (i = from; i <= to; i++) {
+ author = &authors->items[i];
+ authorstat = author->util;
+ items = &authorstat->list;
+ date = string_list_lookup(items, tmp);
+ if (date)
+ subtotal += (uintptr_t)date->util;
+ }
+ htmlf("<td class='%s'>%ld</td>", centerclass, subtotal);
+ total += subtotal;
+ }
+ htmlf("<td class='%s'>%ld</td></tr>", rightclass, total);
+}
+
+static void print_authors(struct string_list *authors, int top,
+ const struct cgit_period *period)
+{
+ struct string_list_item *author;
+ struct authorstat *authorstat;
+ struct string_list *items;
+ struct string_list_item *date;
+ time_t now;
+ long i, j, total;
+ struct tm tm;
+ char *tmp;
+
+ time(&now);
+ gmtime_r(&now, &tm);
+ period->trunc(&tm);
+ for (i = 1; i < period->count; i++)
+ period->dec(&tm);
+
+ html("<table class='stats'><tr><th>Author</th>");
+ for (j = 0; j < period->count; j++) {
+ tmp = period->pretty(&tm);
+ htmlf("<th>%s</th>", tmp);
+ period->inc(&tm);
+ }
+ html("<th>Total</th></tr>\n");
+
+ if (top <= 0 || top > authors->nr)
+ top = authors->nr;
+
+ for (i = 0; i < top; i++) {
+ author = &authors->items[i];
+ html("<tr><td class='left'>");
+ html_txt(author->string);
+ html("</td>");
+ authorstat = author->util;
+ items = &authorstat->list;
+ total = 0;
+ for (j = 0; j < period->count; j++)
+ period->dec(&tm);
+ for (j = 0; j < period->count; j++) {
+ tmp = period->pretty(&tm);
+ period->inc(&tm);
+ date = string_list_lookup(items, tmp);
+ if (!date)
+ html("<td>0</td>");
+ else {
+ htmlf("<td>%lu</td>", (uintptr_t)date->util);
+ total += (uintptr_t)date->util;
+ }
+ }
+ htmlf("<td class='sum'>%ld</td></tr>", total);
+ }
+
+ if (top < authors->nr)
+ print_combined_authorrow(authors, top, authors->nr - 1,
+ "Others (%ld)", "left", "", "sum", period);
+
+ print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
+ "total", "sum", "sum", period);
+ html("</table>");
+}
+
+/* Create a sorted string_list with one entry per author. The util-field
+ * for each author is another string_list which is used to calculate the
+ * number of commits per time-interval.
+ */
+void cgit_show_stats(void)
+{
+ struct string_list authors;
+ const struct cgit_period *period;
+ int top, i;
+ const char *code = "w";
+
+ if (ctx.qry.period)
+ code = ctx.qry.period;
+
+ i = cgit_find_stats_period(code, &period);
+ if (!i) {
+ cgit_print_error_page(404, "Not found",
+ "Unknown statistics type: %c", code[0]);
+ return;
+ }
+ if (i > ctx.repo->max_stats) {
+ cgit_print_error_page(400, "Bad request",
+ "Statistics type disabled: %s", period->name);
+ return;
+ }
+ authors = collect_stats(period);
+ qsort(authors.items, authors.nr, sizeof(struct string_list_item),
+ cmp_total_commits);
+
+ top = ctx.qry.ofs;
+ if (!top)
+ top = 10;
+
+ cgit_print_layout_start();
+ html("<div class='cgit-panel'>");
+ html("<b>stat options</b>");
+ html("<form method='get'>");
+ cgit_add_hidden_formfields(1, 0, "stats");
+ html("<table><tr><td colspan='2'/></tr>");
+ if (ctx.repo->max_stats > 1) {
+ html("<tr><td class='label'>Period:</td>");
+ html("<td class='ctrl'><select name='period' onchange='this.form.submit();'>");
+ for (i = 0; i < ctx.repo->max_stats; i++)
+ html_option(fmt("%c", periods[i].code),
+ periods[i].name, fmt("%c", period->code));
+ html("</select></td></tr>");
+ }
+ html("<tr><td class='label'>Authors:</td>");
+ html("<td class='ctrl'><select name='ofs' onchange='this.form.submit();'>");
+ html_intoption(10, "10", top);
+ html_intoption(25, "25", top);
+ html_intoption(50, "50", top);
+ html_intoption(100, "100", top);
+ html_intoption(-1, "all", top);
+ html("</select></td></tr>");
+ html("<tr><td/><td class='ctrl'>");
+ html("<noscript><input type='submit' value='Reload'/></noscript>");
+ html("</td></tr></table>");
+ html("</form>");
+ html("</div>");
+ htmlf("<h2>Commits per author per %s", period->name);
+ if (ctx.qry.path) {
+ html(" (path '");
+ html_txt(ctx.qry.path);
+ html("')");
+ }
+ html("</h2>");
+ print_authors(&authors, top, period);
+ cgit_print_layout_end();
+}
+
diff --git a/source/ui-stats.h b/source/ui-stats.h
new file mode 100644
index 0000000..0e61b03
--- /dev/null
+++ b/source/ui-stats.h
@@ -0,0 +1,28 @@
+#ifndef UI_STATS_H
+#define UI_STATS_H
+
+#include "cgit.h"
+
+struct cgit_period {
+ const char code;
+ const char *name;
+ int max_periods;
+ int count;
+
+ /* Convert a tm value to the first day in the period */
+ void (*trunc)(struct tm *tm);
+
+ /* Update tm value to start of next/previous period */
+ void (*dec)(struct tm *tm);
+ void (*inc)(struct tm *tm);
+
+ /* Pretty-print a tm value */
+ char *(*pretty)(struct tm *tm);
+};
+
+extern int cgit_find_stats_period(const char *expr, const struct cgit_period **period);
+extern const char *cgit_find_stats_periodname(int idx);
+
+extern void cgit_show_stats(void);
+
+#endif /* UI_STATS_H */
diff --git a/source/ui-summary.c b/source/ui-summary.c
new file mode 100644
index 0000000..947812a
--- /dev/null
+++ b/source/ui-summary.c
@@ -0,0 +1,148 @@
+/* ui-summary.c: functions for generating repo summary page
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-summary.h"
+#include "html.h"
+#include "ui-blob.h"
+#include "ui-log.h"
+#include "ui-plain.h"
+#include "ui-refs.h"
+#include "ui-shared.h"
+
+static int urls;
+
+static void print_url(const char *url)
+{
+ int columns = 3;
+
+ if (ctx.repo->enable_log_filecount)
+ columns++;
+ if (ctx.repo->enable_log_linecount)
+ columns++;
+
+ if (urls++ == 0) {
+ htmlf("<tr class='nohover'><td colspan='%d'>&nbsp;</td></tr>", columns);
+ htmlf("<tr class='nohover'><th class='left' colspan='%d'>Clone</th></tr>\n", columns);
+ }
+
+ htmlf("<tr><td colspan='%d'><a rel='vcs-git' href='", columns);
+ html_url_path(url);
+ html("' title='");
+ html_attr(ctx.repo->name);
+ html(" Git repository'>");
+ html_txt(url);
+ html("</a></td></tr>\n");
+}
+
+void cgit_print_summary(void)
+{
+ int columns = 3;
+
+ if (ctx.repo->enable_log_filecount)
+ columns++;
+ if (ctx.repo->enable_log_linecount)
+ columns++;
+
+ cgit_print_layout_start();
+ html("<table summary='repository info' class='list nowrap'>");
+ cgit_print_branches(ctx.cfg.summary_branches);
+ htmlf("<tr class='nohover'><td colspan='%d'>&nbsp;</td></tr>", columns);
+ cgit_print_tags(ctx.cfg.summary_tags);
+ if (ctx.cfg.summary_log > 0) {
+ htmlf("<tr class='nohover'><td colspan='%d'>&nbsp;</td></tr>", columns);
+ cgit_print_log(ctx.qry.head, 0, ctx.cfg.summary_log, NULL,
+ NULL, NULL, 0, 0, 0);
+ }
+ urls = 0;
+ cgit_add_clone_urls(print_url);
+ html("</table>");
+ cgit_print_layout_end();
+}
+
+/* The caller must free the return value. */
+static char* append_readme_path(const char *filename, const char *ref, const char *path)
+{
+ char *file, *base_dir, *full_path, *resolved_base = NULL, *resolved_full = NULL;
+ /* If a subpath is specified for the about page, make it relative
+ * to the directory containing the configured readme. */
+
+ file = xstrdup(filename);
+ base_dir = dirname(file);
+ if (!strcmp(base_dir, ".") || !strcmp(base_dir, "..")) {
+ if (!ref) {
+ free(file);
+ return NULL;
+ }
+ full_path = xstrdup(path);
+ } else
+ full_path = fmtalloc("%s/%s", base_dir, path);
+
+ if (!ref) {
+ resolved_base = realpath(base_dir, NULL);
+ resolved_full = realpath(full_path, NULL);
+ if (!resolved_base || !resolved_full || !starts_with(resolved_full, resolved_base)) {
+ free(full_path);
+ full_path = NULL;
+ }
+ }
+
+ free(file);
+ free(resolved_base);
+ free(resolved_full);
+
+ return full_path;
+}
+
+void cgit_print_repo_readme(const char *path)
+{
+ char *filename, *ref, *mimetype;
+ int free_filename = 0;
+
+ mimetype = get_mimetype_for_filename(path);
+ if (mimetype && (!strncmp(mimetype, "image/", 6) || !strncmp(mimetype, "video/", 6))) {
+ ctx.page.mimetype = mimetype;
+ ctx.page.charset = NULL;
+ cgit_print_plain();
+ free(mimetype);
+ return;
+ }
+ free(mimetype);
+
+ cgit_print_layout_start();
+ if (ctx.repo->readme.nr == 0)
+ goto done;
+
+ filename = ctx.repo->readme.items[0].string;
+ ref = ctx.repo->readme.items[0].util;
+
+ if (path) {
+ free_filename = 1;
+ filename = append_readme_path(filename, ref, path);
+ if (!filename)
+ goto done;
+ }
+
+ /* Print the calculated readme, either from the git repo or from the
+ * filesystem, while applying the about-filter.
+ */
+ html("<div id='summary'>");
+ cgit_open_filter(ctx.repo->about_filter, filename);
+ if (ref)
+ cgit_print_file(filename, ref, 1);
+ else
+ html_include(filename);
+ cgit_close_filter(ctx.repo->about_filter);
+
+ html("</div>");
+ if (free_filename)
+ free(filename);
+
+done:
+ cgit_print_layout_end();
+}
diff --git a/source/ui-summary.h b/source/ui-summary.h
new file mode 100644
index 0000000..cba696a
--- /dev/null
+++ b/source/ui-summary.h
@@ -0,0 +1,7 @@
+#ifndef UI_SUMMARY_H
+#define UI_SUMMARY_H
+
+extern void cgit_print_summary(void);
+extern void cgit_print_repo_readme(const char *path);
+
+#endif /* UI_SUMMARY_H */
diff --git a/source/ui-tag.c b/source/ui-tag.c
new file mode 100644
index 0000000..8f58f14
--- /dev/null
+++ b/source/ui-tag.c
@@ -0,0 +1,122 @@
+/* ui-tag.c: display a tag
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-tag.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static void print_tag_content(char *buf)
+{
+ char *p;
+
+ if (!buf)
+ return;
+
+ html("<div class='commit-subject'>");
+ p = strchr(buf, '\n');
+ if (p)
+ *p = '\0';
+ html_txt(buf);
+ html("</div>");
+ if (p) {
+ html("<div class='commit-msg'>");
+ html_txt(++p);
+ html("</div>");
+ }
+}
+
+static void print_download_links(char *revname)
+{
+ html("<tr><th>download</th><td class='oid'>");
+ cgit_print_snapshot_links(ctx.repo, revname, "<br/>");
+ html("</td></tr>");
+}
+
+void cgit_print_tag(char *revname)
+{
+ struct strbuf fullref = STRBUF_INIT;
+ struct object_id oid;
+ struct object *obj;
+
+ if (!revname)
+ revname = ctx.qry.head;
+
+ strbuf_addf(&fullref, "refs/tags/%s", revname);
+ if (repo_get_oid(the_repository, fullref.buf, &oid)) {
+ cgit_print_error_page(404, "Not found",
+ "Bad tag reference: %s", revname);
+ goto cleanup;
+ }
+ obj = parse_object(the_repository, &oid);
+ if (!obj) {
+ cgit_print_error_page(500, "Internal server error",
+ "Bad object id: %s", oid_to_hex(&oid));
+ goto cleanup;
+ }
+ if (obj->type == OBJ_TAG) {
+ struct tag *tag;
+ struct taginfo *info;
+
+ tag = lookup_tag(the_repository, &oid);
+ if (!tag || parse_tag(the_repository, tag) || !(info = cgit_parse_tag(tag))) {
+ cgit_print_error_page(500, "Internal server error",
+ "Bad tag object: %s", revname);
+ goto cleanup;
+ }
+ cgit_print_layout_start();
+ html("<table class='commit-info'>\n");
+ html("<tr><td>tag name</td><td>");
+ html_txt(revname);
+ htmlf(" (%s)</td></tr>\n", oid_to_hex(&oid));
+ if (info->tagger_date > 0) {
+ html("<tr><td>tag date</td><td>");
+ html_txt(show_date(info->tagger_date, info->tagger_tz,
+ cgit_date_mode(DATE_ISO8601)));
+ html("</td></tr>\n");
+ }
+ if (info->tagger) {
+ html("<tr><td>tagged by</td><td>");
+ cgit_open_filter(ctx.repo->email_filter, info->tagger_email, "tag");
+ html_txt(info->tagger);
+ if (info->tagger_email && !ctx.cfg.noplainemail) {
+ html(" ");
+ html_txt(info->tagger_email);
+ }
+ cgit_close_filter(ctx.repo->email_filter);
+ html("</td></tr>\n");
+ }
+ html("<tr><td>tagged object</td><td class='oid'>");
+ cgit_object_link(tag->tagged);
+ html("</td></tr>\n");
+ if (ctx.repo->snapshots)
+ print_download_links(revname);
+ html("</table>\n");
+ print_tag_content(info->msg);
+ cgit_print_layout_end();
+ cgit_free_taginfo(info);
+ } else {
+ cgit_print_layout_start();
+ html("<table class='commit-info'>\n");
+ html("<tr><td>tag name</td><td>");
+ html_txt(revname);
+ html("</td></tr>\n");
+ html("<tr><td>tagged object</td><td class='oid'>");
+ cgit_object_link(obj);
+ html("</td></tr>\n");
+ if (ctx.repo->snapshots)
+ print_download_links(revname);
+ html("</table>\n");
+ cgit_print_layout_end();
+ }
+
+cleanup:
+ strbuf_release(&fullref);
+}
diff --git a/source/ui-tag.h b/source/ui-tag.h
new file mode 100644
index 0000000..d295cdc
--- /dev/null
+++ b/source/ui-tag.h
@@ -0,0 +1,6 @@
+#ifndef UI_TAG_H
+#define UI_TAG_H
+
+extern void cgit_print_tag(char *revname);
+
+#endif /* UI_TAG_H */
diff --git a/source/ui-tree.c b/source/ui-tree.c
new file mode 100644
index 0000000..5ac8c9a
--- /dev/null
+++ b/source/ui-tree.c
@@ -0,0 +1,407 @@
+/* ui-tree.c: functions for tree output
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+ #define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-tree.h"
+#include "html.h"
+#include "ui-shared.h"
+
+struct walk_tree_context {
+ char *curr_rev;
+ char *match_path;
+ int state;
+};
+
+static void print_text_buffer(const char *name, char *buf, unsigned long size)
+{
+ unsigned long lineno, idx;
+ const char *numberfmt = "<a id='n%1$d' href='#n%1$d'>%1$d</a>\n";
+
+ html("<table summary='blob content' class='blob'>\n");
+
+ if (ctx.cfg.enable_tree_linenumbers) {
+ html("<tr><td class='linenumbers'><pre>");
+ idx = 0;
+ lineno = 0;
+
+ if (size) {
+ htmlf(numberfmt, ++lineno);
+ while (idx < size - 1) { // skip absolute last newline
+ if (buf[idx] == '\n')
+ htmlf(numberfmt, ++lineno);
+ idx++;
+ }
+ }
+ html("</pre></td>\n");
+ }
+ else {
+ html("<tr>\n");
+ }
+
+ if (ctx.repo->source_filter) {
+ char *filter_arg = xstrdup(name);
+ html("<td class='lines'><pre><code>");
+ cgit_open_filter(ctx.repo->source_filter, filter_arg);
+ html_raw(buf, size);
+ cgit_close_filter(ctx.repo->source_filter);
+ free(filter_arg);
+ html("</code></pre></td></tr></table>\n");
+ return;
+ }
+
+ html("<td class='lines'><pre><code>");
+ html_txt(buf);
+ html("</code></pre></td></tr></table>\n");
+}
+
+#define ROWLEN 32
+
+static void print_binary_buffer(char *buf, unsigned long size)
+{
+ unsigned long ofs, idx;
+ static char ascii[ROWLEN + 1];
+
+ html("<table summary='blob content' class='bin-blob'>\n");
+ html("<tr><th>ofs</th><th>hex dump</th><th>ascii</th></tr>");
+ for (ofs = 0; ofs < size; ofs += ROWLEN, buf += ROWLEN) {
+ htmlf("<tr><td class='right'>%04lx</td><td class='hex'>", ofs);
+ for (idx = 0; idx < ROWLEN && ofs + idx < size; idx++)
+ htmlf("%*s%02x",
+ idx == 16 ? 4 : 1, "",
+ buf[idx] & 0xff);
+ html(" </td><td class='hex'>");
+ for (idx = 0; idx < ROWLEN && ofs + idx < size; idx++)
+ ascii[idx] = isgraph(buf[idx]) ? buf[idx] : '.';
+ ascii[idx] = '\0';
+ html_txt(ascii);
+ html("</td></tr>\n");
+ }
+ html("</table>\n");
+}
+
+static void print_object(const struct object_id *oid, const char *path, const char *basename, const char *rev)
+{
+ enum object_type type;
+ char *buf;
+ unsigned long size;
+ bool is_binary;
+
+ type = odb_read_object_info(the_repository->objects, oid, &size);
+ if (type == OBJ_BAD) {
+ cgit_print_error_page(404, "Not found",
+ "Bad object name: %s", oid_to_hex(oid));
+ return;
+ }
+
+ buf = odb_read_object(the_repository->objects, oid, &type, &size);
+ if (!buf) {
+ cgit_print_error_page(500, "Internal server error",
+ "Error reading object %s", oid_to_hex(oid));
+ return;
+ }
+ is_binary = buffer_is_binary(buf, size);
+
+ cgit_set_title_from_path(path);
+
+ cgit_print_layout_start();
+ htmlf("blob: %s (", oid_to_hex(oid));
+ cgit_plain_link("plain", NULL, NULL, ctx.qry.head,
+ rev, path);
+ if (ctx.repo->enable_blame && !is_binary) {
+ html(") (");
+ cgit_blame_link("blame", NULL, NULL, ctx.qry.head,
+ rev, path);
+ }
+ html(")\n");
+
+ if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) {
+ htmlf("<div class='error'>blob size (%ldKB) exceeds display size limit (%dKB).</div>",
+ size / 1024, ctx.cfg.max_blob_size);
+ return;
+ }
+
+ if (is_binary)
+ print_binary_buffer(buf, size);
+ else
+ print_text_buffer(basename, buf, size);
+
+ free(buf);
+}
+
+struct single_tree_ctx {
+ struct strbuf *path;
+ struct object_id oid;
+ char *name;
+ size_t count;
+};
+
+static int single_tree_cb(const struct object_id *oid, struct strbuf *base,
+ const char *pathname, unsigned mode, void *cbdata)
+{
+ struct single_tree_ctx *ctx = cbdata;
+
+ if (++ctx->count > 1)
+ return -1;
+
+ if (!S_ISDIR(mode)) {
+ ctx->count = 2;
+ return -1;
+ }
+
+ ctx->name = xstrdup(pathname);
+ oidcpy(&ctx->oid, oid);
+ strbuf_addf(ctx->path, "/%s", pathname);
+ return 0;
+}
+
+static void write_tree_link(const struct object_id *oid, char *name,
+ char *rev, struct strbuf *fullpath)
+{
+ size_t initial_length = fullpath->len;
+ struct tree *tree;
+ struct single_tree_ctx tree_ctx = {
+ .path = fullpath,
+ .count = 1,
+ };
+ struct pathspec paths = {
+ .nr = 0
+ };
+
+ oidcpy(&tree_ctx.oid, oid);
+
+ while (tree_ctx.count == 1) {
+ cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, rev,
+ fullpath->buf);
+
+ tree = lookup_tree(the_repository, &tree_ctx.oid);
+ if (!tree)
+ return;
+
+ free(tree_ctx.name);
+ tree_ctx.name = NULL;
+ tree_ctx.count = 0;
+
+ read_tree(the_repository, tree, &paths, single_tree_cb, &tree_ctx);
+
+ if (tree_ctx.count != 1)
+ break;
+
+ html(" / ");
+ name = tree_ctx.name;
+ }
+
+ strbuf_setlen(fullpath, initial_length);
+}
+
+static int ls_item(const struct object_id *oid, struct strbuf *base,
+ const char *pathname, unsigned mode, void *cbdata)
+{
+ struct walk_tree_context *walk_tree_ctx = cbdata;
+ char *name;
+ struct strbuf fullpath = STRBUF_INIT;
+ struct strbuf linkpath = STRBUF_INIT;
+ struct strbuf class = STRBUF_INIT;
+ enum object_type type;
+ unsigned long 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)) {
+ type = odb_read_object_info(the_repository->objects, oid, &size);
+ if (type == OBJ_BAD) {
+ htmlf("<tr><td colspan='3'>Bad object: %s %s</td></tr>",
+ name,
+ oid_to_hex(oid));
+ goto cleanup;
+ }
+ }
+
+ html("<tr><td class='ls-mode'>");
+ cgit_print_filemode(mode);
+ html("</td><td>");
+ if (S_ISGITLINK(mode)) {
+ cgit_submodule_link("ls-mod", fullpath.buf, oid_to_hex(oid));
+ } else if (S_ISDIR(mode)) {
+ write_tree_link(oid, name, walk_tree_ctx->curr_rev,
+ &fullpath);
+ } else {
+ char *ext = strrchr(name, '.');
+ strbuf_addstr(&class, "ls-blob");
+ if (ext)
+ strbuf_addf(&class, " %s", ext + 1);
+ cgit_tree_link(name, NULL, class.buf, ctx.qry.head,
+ walk_tree_ctx->curr_rev, fullpath.buf);
+ }
+ if (S_ISLNK(mode)) {
+ html(" -> ");
+ buf = odb_read_object(the_repository->objects, oid, &type, &size);
+ if (!buf) {
+ htmlf("Error reading object: %s", oid_to_hex(oid));
+ goto cleanup;
+ }
+ strbuf_addbuf(&linkpath, &fullpath);
+ strbuf_addf(&linkpath, "/../%s", buf);
+ strbuf_normalize_path(&linkpath);
+ cgit_tree_link(buf, NULL, class.buf, ctx.qry.head,
+ walk_tree_ctx->curr_rev, linkpath.buf);
+ free(buf);
+ strbuf_release(&linkpath);
+ }
+ htmlf("</td><td class='ls-size'>%li</td>", size);
+
+ html("<td>");
+ cgit_log_link("log", NULL, "button", ctx.qry.head,
+ walk_tree_ctx->curr_rev, fullpath.buf, 0, NULL, NULL,
+ ctx.qry.showmsg, 0);
+ if (ctx.repo->max_stats)
+ cgit_stats_link("stats", NULL, "button", ctx.qry.head,
+ fullpath.buf);
+ if (!S_ISGITLINK(mode))
+ cgit_plain_link("plain", NULL, "button", ctx.qry.head,
+ walk_tree_ctx->curr_rev, fullpath.buf);
+ if (!S_ISDIR(mode) && ctx.repo->enable_blame)
+ cgit_blame_link("blame", NULL, "button", ctx.qry.head,
+ walk_tree_ctx->curr_rev, fullpath.buf);
+ html("</td></tr>\n");
+
+cleanup:
+ free(name);
+ strbuf_release(&fullpath);
+ strbuf_release(&class);
+ return 0;
+}
+
+static void ls_head(void)
+{
+ cgit_print_layout_start();
+ html("<table summary='tree listing' class='list'>\n");
+ html("<tr class='nohover'>");
+ html("<th class='left'>Mode</th>");
+ html("<th class='left'>Name</th>");
+ html("<th class='right'>Size</th>");
+ html("<th/>");
+ html("</tr>\n");
+}
+
+static void ls_tail(void)
+{
+ html("</table>\n");
+ cgit_print_layout_end();
+}
+
+static void ls_tree(const struct object_id *oid, const char *path, struct walk_tree_context *walk_tree_ctx)
+{
+ 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_tree_ctx);
+ ls_tail();
+}
+
+
+static int walk_tree(const struct object_id *oid, struct strbuf *base,
+ const char *pathname, unsigned mode, void *cbdata)
+{
+ struct walk_tree_context *walk_tree_ctx = cbdata;
+
+ if (walk_tree_ctx->state == 0) {
+ struct strbuf buffer = STRBUF_INIT;
+
+ strbuf_addbuf(&buffer, base);
+ strbuf_addstr(&buffer, pathname);
+ if (strcmp(walk_tree_ctx->match_path, buffer.buf))
+ return READ_TREE_RECURSIVE;
+
+ if (S_ISDIR(mode)) {
+ walk_tree_ctx->state = 1;
+ cgit_set_title_from_path(buffer.buf);
+ strbuf_release(&buffer);
+ ls_head();
+ return READ_TREE_RECURSIVE;
+ } else {
+ walk_tree_ctx->state = 2;
+ print_object(oid, buffer.buf, pathname, walk_tree_ctx->curr_rev);
+ strbuf_release(&buffer);
+ return 0;
+ }
+ }
+ ls_item(oid, base, pathname, mode, walk_tree_ctx);
+ return 0;
+}
+
+/*
+ * Show a tree or a blob
+ * rev: the commit pointing at the root tree object
+ * path: path to tree or blob
+ */
+void cgit_print_tree(const char *rev, char *path)
+{
+ struct object_id oid;
+ struct commit *commit;
+ struct pathspec_item path_items = {
+ .match = path,
+ .len = path ? strlen(path) : 0
+ };
+ struct pathspec paths = {
+ .nr = path ? 1 : 0,
+ .items = &path_items
+ };
+ struct walk_tree_context walk_tree_ctx = {
+ .match_path = path,
+ .state = 0
+ };
+
+ if (!rev)
+ rev = ctx.qry.head;
+
+ if (repo_get_oid(the_repository, rev, &oid)) {
+ cgit_print_error_page(404, "Not found",
+ "Invalid revision name: %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",
+ "Invalid commit reference: %s", rev);
+ return;
+ }
+
+ walk_tree_ctx.curr_rev = xstrdup(rev);
+
+ if (path == NULL) {
+ ls_tree(get_commit_tree_oid(commit), NULL, &walk_tree_ctx);
+ goto cleanup;
+ }
+
+ read_tree(the_repository, repo_get_commit_tree(the_repository, commit),
+ &paths, walk_tree, &walk_tree_ctx);
+ if (walk_tree_ctx.state == 1)
+ ls_tail();
+ else if (walk_tree_ctx.state == 2)
+ cgit_print_layout_end();
+ else
+ cgit_print_error_page(404, "Not found", "Path not found");
+
+cleanup:
+ free(walk_tree_ctx.curr_rev);
+}
diff --git a/source/ui-tree.h b/source/ui-tree.h
new file mode 100644
index 0000000..bbd34e3
--- /dev/null
+++ b/source/ui-tree.h
@@ -0,0 +1,6 @@
+#ifndef UI_TREE_H
+#define UI_TREE_H
+
+extern void cgit_print_tree(const char *rev, char *path);
+
+#endif /* UI_TREE_H */