summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--AUTHORS13
-rw-r--r--AUTHORS_705
-rw-r--r--Makefile20
-rw-r--r--NOTES_7041
-rw-r--r--README6
-rw-r--r--README_7047
-rw-r--r--cgit-70.c1079
-rw-r--r--cgit.h2
-rw-r--r--cgit_70.mk155
-rw-r--r--cmd_70.c234
-rw-r--r--shared_70.c578
-rw-r--r--ui-shared_70.c40
-rw-r--r--ui-shared_70.h13
-rw-r--r--ui_70-commit.c171
-rw-r--r--ui_70-diff.c518
-rw-r--r--ui_70-log.c544
-rw-r--r--ui_70-patch.c92
-rw-r--r--ui_70-refs.c218
-rw-r--r--ui_70-repolist.c331
-rw-r--r--ui_70-shared.c1345
-rw-r--r--ui_70-shared.h137
-rw-r--r--ui_70-summary.c144
-rw-r--r--ui_70-tag.c131
-rw-r--r--ui_70-tree.c337
-rw-r--r--ui_70_repolist.c379
26 files changed, 6571 insertions, 10 deletions
diff --git a/.gitignore b/.gitignore
index 661df34..fb3044d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Files I don't care to see in git-status/commit
/cgit
+/cgit-70
cgit.conf
CGIT-CFLAGS
VERSION
diff --git a/AUTHORS b/AUTHORS
index 031de33..a41c135 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,7 +1,14 @@
-Maintainer:
+cgit-70 is developed and maintained by
+ Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+
+cgit-70 is a mod/fork of cgit, based on cgit source code. Details about
+the maintainers and developers of cgit (to whom I am thankful) are
+reported below:
+
+cgit Maintainer:
Jason A. Donenfeld <Jason@zx2c4.com>
-Contributors:
+cgit Contributors:
Jason A. Donenfeld <Jason@zx2c4.com>
Lukas Fleischer <cgit@cryptocrack.de>
Johan Herland <johan@herland.net>
@@ -9,5 +16,5 @@ Contributors:
Ferry Huberts <ferry.huberts@pelagic.nl>
John Keeping <john@keeping.me.uk>
-Previous Maintainer:
+cgit Previous Maintainer:
Lars Hjemli <hjemli@gmail.com>
diff --git a/AUTHORS_70 b/AUTHORS_70
new file mode 100644
index 0000000..75ad29b
--- /dev/null
+++ b/AUTHORS_70
@@ -0,0 +1,5 @@
+cgit-70 is a mod/fork of cgit developed and maintained by
+ Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+
+See the file AUTHORS for more information about cgit developers and
+maintainers.
diff --git a/Makefile b/Makefile
index 05ea71f..d617766 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,10 @@
all::
CGIT_VERSION = v1.2.1
-CGIT_SCRIPT_NAME = cgit.cgi
-CGIT_SCRIPT_PATH = /var/www/htdocs/cgit
+CGIT_SCRIPT_NAME = cgit-70.cgi
+CGIT_SCRIPT_PATH = /var/www/htdocs/cgit-70
CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH)
-CGIT_CONFIG = /etc/cgitrc
+CGIT_CONFIG = /etc/cgit-70rc
CACHE_ROOT = /var/cache/cgit
prefix = /usr/local
libdir = $(prefix)/lib
@@ -70,10 +70,15 @@ endif
.SUFFIXES:
+#all:: cgit
+#
+#cgit:
+# $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) -f ../cgit.mk ../cgit $(EXTRA_GIT_TARGETS) NO_CURL=1
+
all:: cgit
-cgit:
- $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) -f ../cgit.mk ../cgit $(EXTRA_GIT_TARGETS) NO_CURL=1
+cgit:
+ $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) -f ../cgit_70.mk ../cgit-70 $(EXTRA_GIT_TARGETS) NO_CURL=1
sparse:
$(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) -f ../cgit.mk NO_CURL=1 cgit-sparse
@@ -147,7 +152,7 @@ $(DOC_PDF): %.pdf : %.txt
a2x -f pdf cgitrc.5.txt
clean: clean-doc
- $(RM) cgit VERSION CGIT-CFLAGS *.o tags
+ $(RM) cgit-70 VERSION CGIT-CFLAGS *.o tags
$(RM) -r .deps
cleanall: clean
@@ -162,7 +167,8 @@ get-git:
tags:
$(QUIET_TAGS)find . -name '*.[ch]' | xargs ctags
-.PHONY: all cgit git get-git
+#.PHONY: all cgit git get-git
+.PHONY: all cgit-70 git get-git
.PHONY: clean clean-doc cleanall
.PHONY: doc doc-html doc-man doc-pdf
.PHONY: install install-doc install-html install-man install-pdf
diff --git a/NOTES_70 b/NOTES_70
new file mode 100644
index 0000000..8202bbc
--- /dev/null
+++ b/NOTES_70
@@ -0,0 +1,41 @@
+## cgit-gopher
+
+The plan is to equip cgit with a Gopher (RFC 1436) front-end. This would
+be possible by replacing the functions in ui-*.c with appropriate
+functions to generate Gopher contents. In practice, all the html-related
+stuff should be replaced by simple text, and the output simplified (we
+can't have more than one link per line in Gopher).
+
+It seems that ui-tree.c is a good place to start for a proof-of-concept.
+
+(20180724-16:19)
+
+ The PoC works. Now we should produce proper selectors for the stuff in
+ the tree page. In particular:
+
+ - Distinguish between files and directories/links
+ - construct an appropriate selector path (see cgit_tree_link in
+ ui-shared.c)
+
+ N.B.: We don't need to support all the stuff in cgit. In particular,
+ the 'virtual-root' variable might be a bit cumbersome to implement,
+ since we need an explicit way to signal the Gopher server that we
+ need the script (i.e., the presence of a '?' after the name of the CGI
+ script).
+
+(20180725 - 9:30)
+
+ The easiest way to inject a Gopher interface seems to be through
+ cmd.c, since the functions to be called for each action are defined in
+ there. It should be sufficient to provide a gopher-cmd.c and to
+ replace all the ui-*.c files with the corresponding ui70-*.c
+ counterparts which implement the Gopher interface.
+
+ Again, we should start from ui-repolist.c and ui-tree.c, which are the
+ two necessary bits for a viable proof-of-concept.
+
+(20180729 - 6:20)
+
+ The PoC is complete. The cgit-70 cgi script implements all the basic
+ functionalities of cgit. I am sure more is to be done, but the result
+ is not bad at all.
diff --git a/README b/README
index 7a6b4a4..7b98ece 100644
--- a/README
+++ b/README
@@ -1,3 +1,9 @@
+This is the original README for cgit. It is still mostly relevant for
+cgit-70. More specific information about cgit-70 are available in
+README_70.
+
+
+
cgit - CGI for Git
==================
diff --git a/README_70 b/README_70
new file mode 100644
index 0000000..3cf84dc
--- /dev/null
+++ b/README_70
@@ -0,0 +1,47 @@
+ _ _ ____ ___
+ ___ __ _(_) |_|___ / _ \
+ / __/ _` | | __| / / | | |
+ | (_| (_| | | |_ / /| |_| |
+ \___\__, |_|\__|_/ \___/
+ |___/
+ ---------------------------
+
+cgit-70 is a mod/fork of cgit (https://git.zx2c4.com/cgit/), the popular
+web-front-end to git. cgit-70 provides a dynamic interface to git
+repositories over Gopher. I started working on a proof-of-concept of a
+simple gopher interface to git, modifying a few functions in cgit, but
+after the first bits fell nicely into place, I decided to keep going and
+replace as much as possible of the web interface with corresponding
+gopher-friendly stuff. The result is cgit-70.
+
+You can see cgit-70 in action at:
+
+ gopher://cgit.mine.nu/1/cgit-70.cgi
+
+and you can clone the repo at:
+
+ http://cgit.mine.nu/cgit-70
+
+All you need to run cgit-70 is a gopher server that supports the same
+CGI interface defined by geomyidae (http://git.r-36.net/geomyidae/). If
+you like shell-stuff, you could also give a try to gosher
+(http://cgit.mine.nu/gosher), which implements the same interface.
+
+ **** DISCLAIMER ****
+
+cgit-70 is based on source code from cgit and is distributed under the
+terms of the GNU General Public License v2. Please read the COPYING file
+shipped with the sources.
+
+cgit is a rock-solid well-tested piece of software, but cgit-70 must be
+considered alpha software. Use it at your own risk, and feel free to
+send patches over if you find a bug (which is quite probable to happen,
+at this stage).
+
+cgit-70 is based on cgit. The cgit-70 files modified from the cgit
+originals are:
+ (C) 2006-2018 cgit Development Team <cgit@lists.zx2c4.com>
+ (C) 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+
+cgit is (C) 2006-2018 cgit Development Team <cgit@lists.zx2c4.com>
+
diff --git a/cgit-70.c b/cgit-70.c
new file mode 100644
index 0000000..714ac1c
--- /dev/null
+++ b/cgit-70.c
@@ -0,0 +1,1079 @@
+/* cgit.c: cgi for the git scm
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "cache.h"
+#include "cmd.h"
+#include "configfile.h"
+#include "html.h"
+#include "ui_70-shared.h"
+#include "ui-stats.h"
+#include "ui-blob.h"
+#include "ui-summary.h"
+#include "scan-tree.h"
+
+const char *cgit_version = CGIT_VERSION;
+
+static void add_mimetype(const char *name, const char *value)
+{
+ struct string_list_item *item;
+
+ item = string_list_insert(&ctx.cfg.mimetypes, name);
+ item->util = xstrdup(value);
+}
+
+static void process_cached_repolist(const char *path);
+
+static void repo_config(struct cgit_repo *repo, const char *name, const char *value)
+{
+ const char *path;
+ struct string_list_item *item;
+
+ if (!strcmp(name, "name"))
+ repo->name = xstrdup(value);
+ else if (!strcmp(name, "clone-url"))
+ repo->clone_url = xstrdup(value);
+ else if (!strcmp(name, "desc"))
+ repo->desc = xstrdup(value);
+ else if (!strcmp(name, "owner"))
+ repo->owner = xstrdup(value);
+ else if (!strcmp(name, "homepage"))
+ repo->homepage = xstrdup(value);
+ else if (!strcmp(name, "defbranch"))
+ repo->defbranch = xstrdup(value);
+ else if (!strcmp(name, "extra-head-content"))
+ repo->extra_head_content = xstrdup(value);
+ else if (!strcmp(name, "snapshots"))
+ repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value);
+ else if (!strcmp(name, "enable-commit-graph"))
+ repo->enable_commit_graph = atoi(value);
+ else if (!strcmp(name, "enable-log-filecount"))
+ repo->enable_log_filecount = atoi(value);
+ else if (!strcmp(name, "enable-log-linecount"))
+ repo->enable_log_linecount = atoi(value);
+ else if (!strcmp(name, "enable-remote-branches"))
+ repo->enable_remote_branches = atoi(value);
+ else if (!strcmp(name, "enable-subject-links"))
+ repo->enable_subject_links = atoi(value);
+ else if (!strcmp(name, "enable-html-serving"))
+ repo->enable_html_serving = atoi(value);
+ else if (!strcmp(name, "branch-sort")) {
+ if (!strcmp(value, "age"))
+ repo->branch_sort = 1;
+ if (!strcmp(value, "name"))
+ repo->branch_sort = 0;
+ } else if (!strcmp(name, "commit-sort")) {
+ if (!strcmp(value, "date"))
+ repo->commit_sort = 1;
+ if (!strcmp(value, "topo"))
+ repo->commit_sort = 2;
+ } else if (!strcmp(name, "max-stats"))
+ repo->max_stats = cgit_find_stats_period(value, NULL);
+ else if (!strcmp(name, "module-link"))
+ repo->module_link= xstrdup(value);
+ else if (skip_prefix(name, "module-link.", &path)) {
+ item = string_list_append(&repo->submodules, xstrdup(path));
+ item->util = xstrdup(value);
+ } else if (!strcmp(name, "section"))
+ repo->section = xstrdup(value);
+ else if (!strcmp(name, "snapshot-prefix"))
+ repo->snapshot_prefix = xstrdup(value);
+ else if (!strcmp(name, "readme") && value != NULL) {
+ if (repo->readme.items == ctx.cfg.readme.items)
+ memset(&repo->readme, 0, sizeof(repo->readme));
+ string_list_append(&repo->readme, xstrdup(value));
+ } else if (!strcmp(name, "logo") && value != NULL)
+ repo->logo = xstrdup(value);
+ else if (!strcmp(name, "logo-link") && value != NULL)
+ repo->logo_link = xstrdup(value);
+ else if (!strcmp(name, "hide"))
+ repo->hide = atoi(value);
+ else if (!strcmp(name, "ignore"))
+ repo->ignore = atoi(value);
+ else if (ctx.cfg.enable_filter_overrides) {
+ if (!strcmp(name, "about-filter"))
+ repo->about_filter = cgit_new_filter(value, ABOUT);
+ else if (!strcmp(name, "commit-filter"))
+ repo->commit_filter = cgit_new_filter(value, COMMIT);
+ else if (!strcmp(name, "source-filter"))
+ repo->source_filter = cgit_new_filter(value, SOURCE);
+ else if (!strcmp(name, "email-filter"))
+ repo->email_filter = cgit_new_filter(value, EMAIL);
+ else if (!strcmp(name, "owner-filter"))
+ repo->owner_filter = cgit_new_filter(value, OWNER);
+ }
+}
+
+static void config_cb(const char *name, const char *value)
+{
+ const char *arg;
+
+ if (!strcmp(name, "section"))
+ ctx.cfg.section = xstrdup(value);
+ else if (!strcmp(name, "repo.url"))
+ ctx.repo = cgit_add_repo(value);
+ else if (ctx.repo && !strcmp(name, "repo.path"))
+ ctx.repo->path = trim_end(value, '/');
+ else if (ctx.repo && skip_prefix(name, "repo.", &arg))
+ repo_config(ctx.repo, arg, value);
+ else if (!strcmp(name, "readme"))
+ string_list_append(&ctx.cfg.readme, xstrdup(value));
+ else if (!strcmp(name, "root-title"))
+ ctx.cfg.root_title = xstrdup(value);
+ else if (!strcmp(name, "root-desc"))
+ ctx.cfg.root_desc = xstrdup(value);
+ else if (!strcmp(name, "root-readme"))
+ ctx.cfg.root_readme = xstrdup(value);
+ else if (!strcmp(name, "css"))
+ ctx.cfg.css = xstrdup(value);
+ else if (!strcmp(name, "favicon"))
+ ctx.cfg.favicon = xstrdup(value);
+ else if (!strcmp(name, "footer"))
+ ctx.cfg.footer = xstrdup(value);
+ else if (!strcmp(name, "head-include"))
+ ctx.cfg.head_include = xstrdup(value);
+ else if (!strcmp(name, "header"))
+ ctx.cfg.header = xstrdup(value);
+ else if (!strcmp(name, "logo"))
+ ctx.cfg.logo = xstrdup(value);
+ else if (!strcmp(name, "logo-link"))
+ ctx.cfg.logo_link = xstrdup(value);
+ else if (!strcmp(name, "module-link"))
+ ctx.cfg.module_link = xstrdup(value);
+ else if (!strcmp(name, "strict-export"))
+ ctx.cfg.strict_export = xstrdup(value);
+ else if (!strcmp(name, "virtual-root"))
+ ctx.cfg.virtual_root = ensure_end(value, '/');
+ else if (!strcmp(name, "noplainemail"))
+ ctx.cfg.noplainemail = atoi(value);
+ else if (!strcmp(name, "noheader"))
+ ctx.cfg.noheader = atoi(value);
+ else if (!strcmp(name, "snapshots"))
+ ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
+ else if (!strcmp(name, "enable-filter-overrides"))
+ ctx.cfg.enable_filter_overrides = atoi(value);
+ else if (!strcmp(name, "enable-follow-links"))
+ ctx.cfg.enable_follow_links = atoi(value);
+ else if (!strcmp(name, "enable-http-clone"))
+ ctx.cfg.enable_http_clone = atoi(value);
+ else if (!strcmp(name, "enable-index-links"))
+ ctx.cfg.enable_index_links = atoi(value);
+ else if (!strcmp(name, "enable-index-owner"))
+ ctx.cfg.enable_index_owner = atoi(value);
+ else if (!strcmp(name, "enable-blame"))
+ ctx.cfg.enable_blame = atoi(value);
+ else if (!strcmp(name, "enable-commit-graph"))
+ ctx.cfg.enable_commit_graph = atoi(value);
+ else if (!strcmp(name, "enable-log-filecount"))
+ ctx.cfg.enable_log_filecount = atoi(value);
+ else if (!strcmp(name, "enable-log-linecount"))
+ ctx.cfg.enable_log_linecount = atoi(value);
+ else if (!strcmp(name, "enable-remote-branches"))
+ ctx.cfg.enable_remote_branches = atoi(value);
+ else if (!strcmp(name, "enable-subject-links"))
+ ctx.cfg.enable_subject_links = atoi(value);
+ else if (!strcmp(name, "enable-html-serving"))
+ ctx.cfg.enable_html_serving = atoi(value);
+ else if (!strcmp(name, "enable-tree-linenumbers"))
+ ctx.cfg.enable_tree_linenumbers = atoi(value);
+ else if (!strcmp(name, "enable-git-config"))
+ ctx.cfg.enable_git_config = atoi(value);
+ else if (!strcmp(name, "max-stats"))
+ ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
+ else if (!strcmp(name, "cache-size"))
+ ctx.cfg.cache_size = atoi(value);
+ else if (!strcmp(name, "cache-root"))
+ ctx.cfg.cache_root = xstrdup(expand_macros(value));
+ else if (!strcmp(name, "cache-root-ttl"))
+ ctx.cfg.cache_root_ttl = atoi(value);
+ else if (!strcmp(name, "cache-repo-ttl"))
+ ctx.cfg.cache_repo_ttl = atoi(value);
+ else if (!strcmp(name, "cache-scanrc-ttl"))
+ ctx.cfg.cache_scanrc_ttl = atoi(value);
+ else if (!strcmp(name, "cache-static-ttl"))
+ ctx.cfg.cache_static_ttl = atoi(value);
+ else if (!strcmp(name, "cache-dynamic-ttl"))
+ ctx.cfg.cache_dynamic_ttl = atoi(value);
+ else if (!strcmp(name, "cache-about-ttl"))
+ ctx.cfg.cache_about_ttl = atoi(value);
+ else if (!strcmp(name, "cache-snapshot-ttl"))
+ ctx.cfg.cache_snapshot_ttl = atoi(value);
+ else if (!strcmp(name, "case-sensitive-sort"))
+ ctx.cfg.case_sensitive_sort = atoi(value);
+ else if (!strcmp(name, "about-filter"))
+ ctx.cfg.about_filter = cgit_new_filter(value, ABOUT);
+ else if (!strcmp(name, "commit-filter"))
+ ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT);
+ else if (!strcmp(name, "email-filter"))
+ ctx.cfg.email_filter = cgit_new_filter(value, EMAIL);
+ else if (!strcmp(name, "owner-filter"))
+ ctx.cfg.owner_filter = cgit_new_filter(value, OWNER);
+ else if (!strcmp(name, "auth-filter"))
+ ctx.cfg.auth_filter = cgit_new_filter(value, AUTH);
+ else if (!strcmp(name, "embedded"))
+ ctx.cfg.embedded = atoi(value);
+ else if (!strcmp(name, "max-atom-items"))
+ ctx.cfg.max_atom_items = atoi(value);
+ else if (!strcmp(name, "max-message-length"))
+ ctx.cfg.max_msg_len = atoi(value);
+ else if (!strcmp(name, "max-repodesc-length"))
+ ctx.cfg.max_repodesc_len = atoi(value);
+ else if (!strcmp(name, "max-blob-size"))
+ ctx.cfg.max_blob_size = atoi(value);
+ else if (!strcmp(name, "max-repo-count"))
+ ctx.cfg.max_repo_count = atoi(value);
+ else if (!strcmp(name, "max-commit-count"))
+ ctx.cfg.max_commit_count = atoi(value);
+ else if (!strcmp(name, "project-list"))
+ ctx.cfg.project_list = xstrdup(expand_macros(value));
+ else if (!strcmp(name, "scan-path"))
+ if (ctx.cfg.cache_size)
+ process_cached_repolist(expand_macros(value));
+ else if (ctx.cfg.project_list)
+ scan_projects(expand_macros(value),
+ ctx.cfg.project_list, repo_config);
+ else
+ scan_tree(expand_macros(value), repo_config);
+ else if (!strcmp(name, "scan-hidden-path"))
+ ctx.cfg.scan_hidden_path = atoi(value);
+ else if (!strcmp(name, "section-from-path"))
+ ctx.cfg.section_from_path = atoi(value);
+ else if (!strcmp(name, "repository-sort"))
+ ctx.cfg.repository_sort = xstrdup(value);
+ else if (!strcmp(name, "section-sort"))
+ ctx.cfg.section_sort = atoi(value);
+ else if (!strcmp(name, "source-filter"))
+ ctx.cfg.source_filter = cgit_new_filter(value, SOURCE);
+ else if (!strcmp(name, "summary-log"))
+ ctx.cfg.summary_log = atoi(value);
+ else if (!strcmp(name, "summary-branches"))
+ ctx.cfg.summary_branches = atoi(value);
+ else if (!strcmp(name, "summary-tags"))
+ ctx.cfg.summary_tags = atoi(value);
+ else if (!strcmp(name, "side-by-side-diffs"))
+ ctx.cfg.difftype = atoi(value) ? DIFF_SSDIFF : DIFF_UNIFIED;
+ else if (!strcmp(name, "agefile"))
+ ctx.cfg.agefile = xstrdup(value);
+ else if (!strcmp(name, "mimetype-file"))
+ ctx.cfg.mimetype_file = xstrdup(value);
+ else if (!strcmp(name, "renamelimit"))
+ ctx.cfg.renamelimit = atoi(value);
+ else if (!strcmp(name, "remove-suffix"))
+ ctx.cfg.remove_suffix = atoi(value);
+ else if (!strcmp(name, "robots"))
+ ctx.cfg.robots = xstrdup(value);
+ else if (!strcmp(name, "clone-prefix"))
+ ctx.cfg.clone_prefix = xstrdup(value);
+ else if (!strcmp(name, "clone-url"))
+ ctx.cfg.clone_url = xstrdup(value);
+ else if (!strcmp(name, "local-time"))
+ ctx.cfg.local_time = atoi(value);
+ else if (!strcmp(name, "commit-sort")) {
+ if (!strcmp(value, "date"))
+ ctx.cfg.commit_sort = 1;
+ if (!strcmp(value, "topo"))
+ ctx.cfg.commit_sort = 2;
+ } else if (!strcmp(name, "branch-sort")) {
+ if (!strcmp(value, "age"))
+ ctx.cfg.branch_sort = 1;
+ if (!strcmp(value, "name"))
+ ctx.cfg.branch_sort = 0;
+ } else if (skip_prefix(name, "mimetype.", &arg))
+ add_mimetype(arg, value);
+ else if (!strcmp(name, "include"))
+ parse_configfile(expand_macros(value), config_cb);
+}
+
+static void querystring_cb(const char *name, const char *value)
+{
+ if (!value)
+ value = "";
+
+ if (!strcmp(name,"r")) {
+ ctx.qry.repo = xstrdup(value);
+ ctx.repo = cgit_get_repoinfo(value);
+ } else if (!strcmp(name, "p")) {
+ ctx.qry.page = xstrdup(value);
+ } else if (!strcmp(name, "url")) {
+ if (*value == '/')
+ value++;
+ ctx.qry.url = xstrdup(value);
+ cgit_parse_url(value);
+ } else if (!strcmp(name, "qt")) {
+ ctx.qry.grep = xstrdup(value);
+ } else if (!strcmp(name, "q")) {
+ ctx.qry.search = xstrdup(value);
+ } else if (!strcmp(name, "h")) {
+ ctx.qry.head = xstrdup(value);
+ ctx.qry.has_symref = 1;
+ } else if (!strcmp(name, "id")) {
+ ctx.qry.sha1 = xstrdup(value);
+ ctx.qry.has_sha1 = 1;
+ } else if (!strcmp(name, "id2")) {
+ ctx.qry.sha2 = xstrdup(value);
+ ctx.qry.has_sha1 = 1;
+ } else if (!strcmp(name, "ofs")) {
+ ctx.qry.ofs = atoi(value);
+ } else if (!strcmp(name, "path")) {
+ ctx.qry.path = trim_end(value, '/');
+ } else if (!strcmp(name, "name")) {
+ ctx.qry.name = xstrdup(value);
+ } else if (!strcmp(name, "s")) {
+ ctx.qry.sort = xstrdup(value);
+ } else if (!strcmp(name, "showmsg")) {
+ ctx.qry.showmsg = atoi(value);
+ } else if (!strcmp(name, "period")) {
+ ctx.qry.period = xstrdup(value);
+ } else if (!strcmp(name, "dt")) {
+ ctx.qry.difftype = atoi(value);
+ ctx.qry.has_difftype = 1;
+ } else if (!strcmp(name, "ss")) {
+ /* No longer generated, but there may be links out there. */
+ ctx.qry.difftype = atoi(value) ? DIFF_SSDIFF : DIFF_UNIFIED;
+ ctx.qry.has_difftype = 1;
+ } else if (!strcmp(name, "all")) {
+ ctx.qry.show_all = atoi(value);
+ } else if (!strcmp(name, "context")) {
+ ctx.qry.context = atoi(value);
+ } else if (!strcmp(name, "ignorews")) {
+ ctx.qry.ignorews = atoi(value);
+ } else if (!strcmp(name, "follow")) {
+ ctx.qry.follow = atoi(value);
+ }
+}
+
+static void prepare_context(void)
+{
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.cfg.agefile = "info/web/last-modified";
+ ctx.cfg.cache_size = 0;
+ ctx.cfg.cache_max_create_time = 5;
+ ctx.cfg.cache_root = CGIT_CACHE_ROOT;
+ ctx.cfg.cache_about_ttl = 15;
+ ctx.cfg.cache_snapshot_ttl = 5;
+ ctx.cfg.cache_repo_ttl = 5;
+ ctx.cfg.cache_root_ttl = 5;
+ ctx.cfg.cache_scanrc_ttl = 15;
+ ctx.cfg.cache_dynamic_ttl = 5;
+ ctx.cfg.cache_static_ttl = -1;
+ ctx.cfg.case_sensitive_sort = 1;
+ ctx.cfg.branch_sort = 0;
+ ctx.cfg.commit_sort = 0;
+ ctx.cfg.css = "/cgit.css";
+ ctx.cfg.logo = "/cgit.png";
+ ctx.cfg.favicon = "/favicon.ico";
+ ctx.cfg.local_time = 0;
+ ctx.cfg.enable_http_clone = 1;
+ ctx.cfg.enable_index_owner = 1;
+ ctx.cfg.enable_tree_linenumbers = 1;
+ ctx.cfg.enable_git_config = 0;
+ ctx.cfg.max_repo_count = 50;
+ ctx.cfg.max_commit_count = 50;
+ ctx.cfg.max_lock_attempts = 5;
+ ctx.cfg.max_msg_len = 80;
+ ctx.cfg.max_repodesc_len = 80;
+ ctx.cfg.max_blob_size = 0;
+ ctx.cfg.max_stats = 0;
+ ctx.cfg.project_list = NULL;
+ ctx.cfg.renamelimit = -1;
+ ctx.cfg.remove_suffix = 0;
+ ctx.cfg.robots = "index, nofollow";
+ ctx.cfg.root_title = "cgit-70 -- Git repository browser over Gopher";
+ ctx.cfg.root_desc = "a fast Gopher interface for the git dscm";
+ ctx.cfg.scan_hidden_path = 0;
+ ctx.cfg.script_name = CGIT_SCRIPT_NAME;
+ ctx.cfg.section = "";
+ ctx.cfg.repository_sort = "name";
+ ctx.cfg.section_sort = 1;
+ ctx.cfg.summary_branches = 10;
+ ctx.cfg.summary_log = 10;
+ ctx.cfg.summary_tags = 10;
+ ctx.cfg.max_atom_items = 10;
+ ctx.cfg.difftype = DIFF_UNIFIED;
+ ctx.env.cgit_config = getenv("CGIT_CONFIG");
+ ctx.env.http_host = getenv("HTTP_HOST");
+ ctx.env.https = getenv("HTTPS");
+ ctx.env.no_http = getenv("NO_HTTP");
+ ctx.env.path_info = getenv("PATH_INFO");
+ ctx.env.query_string = getenv("QUERY_STRING");
+ ctx.env.request_method = getenv("REQUEST_METHOD");
+ ctx.env.script_name = getenv("SCRIPT_NAME");
+ ctx.env.server_name = getenv("SERVER_NAME");
+ ctx.env.server_port = getenv("SERVER_PORT");
+ ctx.env.http_cookie = getenv("HTTP_COOKIE");
+ ctx.env.http_referer = getenv("HTTP_REFERER");
+ ctx.env.content_length = getenv("CONTENT_LENGTH") ? strtoul(getenv("CONTENT_LENGTH"), NULL, 10) : 0;
+ ctx.env.authenticated = 0;
+ ctx.page.mimetype = "text/html";
+ ctx.page.charset = PAGE_ENCODING;
+ ctx.page.filename = NULL;
+ ctx.page.size = 0;
+ ctx.page.modified = time(NULL);
+ ctx.page.expires = ctx.page.modified;
+ ctx.page.etag = NULL;
+ string_list_init(&ctx.cfg.mimetypes, 1);
+ if (ctx.env.script_name)
+ ctx.cfg.script_name = xstrdup(ctx.env.script_name);
+ if (ctx.env.query_string)
+ ctx.qry.raw = xstrdup(ctx.env.query_string);
+ if (!ctx.env.cgit_config)
+ ctx.env.cgit_config = CGIT_CONFIG;
+}
+
+struct refmatch {
+ char *req_ref;
+ char *first_ref;
+ int match;
+};
+
+static int find_current_ref(const char *refname, const struct object_id *oid,
+ int flags, void *cb_data)
+{
+ struct refmatch *info;
+
+ info = (struct refmatch *)cb_data;
+ if (!strcmp(refname, info->req_ref))
+ info->match = 1;
+ if (!info->first_ref)
+ info->first_ref = xstrdup(refname);
+ return info->match;
+}
+
+static void free_refmatch_inner(struct refmatch *info)
+{
+ if (info->first_ref)
+ free(info->first_ref);
+}
+
+static char *find_default_branch(struct cgit_repo *repo)
+{
+ struct refmatch info;
+ char *ref;
+
+ info.req_ref = repo->defbranch;
+ info.first_ref = NULL;
+ info.match = 0;
+ for_each_branch_ref(find_current_ref, &info);
+ if (info.match)
+ ref = info.req_ref;
+ else
+ ref = info.first_ref;
+ if (ref)
+ ref = xstrdup(ref);
+ free_refmatch_inner(&info);
+
+ return ref;
+}
+
+static char *guess_defbranch(void)
+{
+ const char *ref, *refname;
+ struct object_id oid;
+
+ ref = resolve_ref_unsafe("HEAD", 0, &oid, NULL);
+ if (!ref || !skip_prefix(ref, "refs/heads/", &refname))
+ return "master";
+ return xstrdup(refname);
+}
+
+/* The caller must free filename and ref after calling this. */
+static inline void parse_readme(const char *readme, char **filename, char **ref, struct cgit_repo *repo)
+{
+ const char *colon;
+
+ *filename = NULL;
+ *ref = NULL;
+
+ if (!readme || !readme[0])
+ return;
+
+ /* Check if the readme is tracked in the git repo. */
+ colon = strchr(readme, ':');
+ if (colon && strlen(colon) > 1) {
+ /* If it starts with a colon, we want to use
+ * the default branch */
+ if (colon == readme && repo->defbranch)
+ *ref = xstrdup(repo->defbranch);
+ else
+ *ref = xstrndup(readme, colon - readme);
+ readme = colon + 1;
+ }
+
+ /* Prepend repo path to relative readme path unless tracked. */
+ if (!(*ref) && readme[0] != '/')
+ *filename = fmtalloc("%s/%s", repo->path, readme);
+ else
+ *filename = xstrdup(readme);
+}
+static void choose_readme(struct cgit_repo *repo)
+{
+ int found;
+ char *filename, *ref;
+ struct string_list_item *entry;
+
+ if (!repo->readme.nr)
+ return;
+
+ found = 0;
+ for_each_string_list_item(entry, &repo->readme) {
+ parse_readme(entry->string, &filename, &ref, repo);
+ if (!filename) {
+ free(filename);
+ free(ref);
+ continue;
+ }
+ if (ref) {
+ if (cgit_ref_path_exists(filename, ref, 1)) {
+ found = 1;
+ break;
+ }
+ }
+ else if (!access(filename, R_OK)) {
+ found = 1;
+ break;
+ }
+ free(filename);
+ free(ref);
+ }
+ repo->readme.strdup_strings = 1;
+ string_list_clear(&repo->readme, 0);
+ repo->readme.strdup_strings = 0;
+ if (found)
+ string_list_append(&repo->readme, filename)->util = ref;
+}
+
+static void print_no_repo_clone_urls(const char *url)
+{
+ html("<tr><td><a rel='vcs-git' href='");
+ html_url_path(url);
+ html("' title='");
+ html_attr(ctx.repo->name);
+ html(" Git repository'>");
+ html_txt(url);
+ html("</a></td></tr>\n");
+}
+
+static void prepare_repo_env(int *nongit)
+{
+ /* The path to the git repository. */
+ setenv("GIT_DIR", ctx.repo->path, 1);
+
+ /* Do not look in /etc/ for gitconfig and gitattributes. */
+ setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
+ setenv("GIT_ATTR_NOSYSTEM", "1", 1);
+ unsetenv("HOME");
+ unsetenv("XDG_CONFIG_HOME");
+
+ /* Setup the git directory and initialize the notes system. Both of these
+ * load local configuration from the git repository, so we do them both while
+ * the HOME variables are unset. */
+ setup_git_directory_gently(nongit);
+ init_display_notes(NULL);
+}
+static int prepare_repo_cmd(int nongit)
+{
+ struct object_id oid;
+ int rc;
+
+ if (nongit) {
+ const char *name = ctx.repo->name;
+ rc = errno;
+ ctx.page.title = fmtalloc("%s - %s", ctx.cfg.root_title,
+ "config error");
+ ctx.repo = NULL;
+ cgit_print_http_headers();
+ cgit_print_docstart();
+ cgit_print_pageheader();
+ cgit_print_error("Failed to open %s: %s", name,
+ rc ? strerror(rc) : "Not a valid git repository");
+ cgit_print_docend();
+ return 1;
+ }
+ ctx.page.title = fmtalloc("%s - %s", ctx.repo->name, ctx.repo->desc);
+
+ if (!ctx.repo->defbranch)
+ ctx.repo->defbranch = guess_defbranch();
+
+ if (!ctx.qry.head) {
+ ctx.qry.nohead = 1;
+ ctx.qry.head = find_default_branch(ctx.repo);
+ }
+
+ if (!ctx.qry.head) {
+ cgit_print_http_headers();
+ cgit_print_docstart();
+ cgit_print_pageheader();
+ cgit_print_error("Repository seems to be empty");
+ if (!strcmp(ctx.qry.page, "summary")) {
+ html("<table class='list'><tr class='nohover'><td>&nbsp;</td></tr><tr class='nohover'><th class='left'>Clone</th></tr>\n");
+ cgit_prepare_repo_env(ctx.repo);
+ cgit_add_clone_urls(print_no_repo_clone_urls);
+ html("</table>\n");
+ }
+ cgit_print_docend();
+ return 1;
+ }
+
+ if (get_oid(ctx.qry.head, &oid)) {
+ char *old_head = ctx.qry.head;
+ ctx.qry.head = xstrdup(ctx.repo->defbranch);
+ cgit_print_error_page(404, "Not found",
+ "Invalid branch: %s", old_head);
+ free(old_head);
+ return 1;
+ }
+ string_list_sort(&ctx.repo->submodules);
+ cgit_prepare_repo_env(ctx.repo);
+ choose_readme(ctx.repo);
+ return 0;
+}
+
+static inline void open_auth_filter(const char *function)
+{
+ cgit_open_filter(ctx.cfg.auth_filter, function,
+ ctx.env.http_cookie ? ctx.env.http_cookie : "",
+ ctx.env.request_method ? ctx.env.request_method : "",
+ ctx.env.query_string ? ctx.env.query_string : "",
+ ctx.env.http_referer ? ctx.env.http_referer : "",
+ ctx.env.path_info ? ctx.env.path_info : "",
+ ctx.env.http_host ? ctx.env.http_host : "",
+ ctx.env.https ? ctx.env.https : "",
+ ctx.qry.repo ? ctx.qry.repo : "",
+ ctx.qry.page ? ctx.qry.page : "",
+ ctx.qry.url ? ctx.qry.url : "",
+ cgit_loginurl());
+}
+
+/* We intentionally keep this rather small, instead of looping and
+ * feeding it to the filter a couple bytes at a time. This way, the
+ * filter itself does not need to handle any denial of service or
+ * buffer bloat issues. If this winds up being too small, people
+ * will complain on the mailing list, and we'll increase it as needed. */
+#define MAX_AUTHENTICATION_POST_BYTES 4096
+/* The filter is expected to spit out "Status: " and all headers. */
+static inline void authenticate_post(void)
+{
+ char buffer[MAX_AUTHENTICATION_POST_BYTES];
+ ssize_t len;
+
+ open_auth_filter("authenticate-post");
+ len = ctx.env.content_length;
+ if (len > MAX_AUTHENTICATION_POST_BYTES)
+ len = MAX_AUTHENTICATION_POST_BYTES;
+ if ((len = read(STDIN_FILENO, buffer, len)) < 0)
+ die_errno("Could not read POST from stdin");
+ if (write(STDOUT_FILENO, buffer, len) < 0)
+ die_errno("Could not write POST to stdout");
+ cgit_close_filter(ctx.cfg.auth_filter);
+ exit(0);
+}
+
+static inline void authenticate_cookie(void)
+{
+ /* If we don't have an auth_filter, consider all cookies valid, and thus return early. */
+ if (!ctx.cfg.auth_filter) {
+ ctx.env.authenticated = 1;
+ return;
+ }
+
+ /* If we're having something POST'd to /login, we're authenticating POST,
+ * instead of the cookie, so call authenticate_post and bail out early.
+ * This pattern here should match /?p=login with POST. */
+ if (ctx.env.request_method && ctx.qry.page && !ctx.repo && \
+ !strcmp(ctx.env.request_method, "POST") && !strcmp(ctx.qry.page, "login")) {
+ authenticate_post();
+ return;
+ }
+
+ /* If we've made it this far, we're authenticating the cookie for real, so do that. */
+ open_auth_filter("authenticate-cookie");
+ ctx.env.authenticated = cgit_close_filter(ctx.cfg.auth_filter);
+}
+
+static void process_request(void)
+{
+ struct cgit_cmd *cmd;
+ int nongit = 0;
+
+ /* If we're not yet authenticated, no matter what page we're on,
+ * display the authentication body from the auth_filter. This should
+ * never be cached. */
+/* if (!ctx.env.authenticated) {
+ ctx.page.title = "Authentication Required";
+ cgit_print_http_headers();
+ cgit_print_docstart();
+ cgit_print_pageheader();
+ open_auth_filter("body");
+ cgit_close_filter(ctx.cfg.auth_filter);
+ cgit_print_docend();
+ return;
+ }
+*/
+ if (ctx.repo)
+ prepare_repo_env(&nongit);
+
+ cmd = cgit_get_cmd();
+ if (!cmd) {
+ ctx.page.title = "cgit error";
+ cgit_gopher_error("Invalid request");
+ return;
+ }
+
+ if (!ctx.cfg.enable_http_clone && cmd->is_clone) {
+ ctx.page.title = "cgit error";
+ cgit_gopher_error("Invalid request");
+ return;
+ }
+
+ if (cmd->want_repo && !ctx.repo) {
+ cgit_gopher_error("No repository selected");
+ return;
+ }
+
+ /* If cmd->want_vpath is set, assume ctx.qry.path contains a "virtual"
+ * in-project path limit to be made available at ctx.qry.vpath.
+ * Otherwise, no path limit is in effect (ctx.qry.vpath = NULL).
+ */
+ ctx.qry.vpath = cmd->want_vpath ? ctx.qry.path : NULL;
+
+ if (ctx.repo && prepare_repo_cmd(nongit))
+ return;
+
+ cmd->fn();
+}
+
+static int cmp_repos(const void *a, const void *b)
+{
+ const struct cgit_repo *ra = a, *rb = b;
+ return strcmp(ra->url, rb->url);
+}
+
+static char *build_snapshot_setting(int bitmap)
+{
+ const struct cgit_snapshot_format *f;
+ struct strbuf result = STRBUF_INIT;
+
+ for (f = cgit_snapshot_formats; f->suffix; f++) {
+ if (cgit_snapshot_format_bit(f) & bitmap) {
+ if (result.len)
+ strbuf_addch(&result, ' ');
+ strbuf_addstr(&result, f->suffix);
+ }
+ }
+ return strbuf_detach(&result, NULL);
+}
+
+static char *get_first_line(char *txt)
+{
+ char *t = xstrdup(txt);
+ char *p = strchr(t, '\n');
+ if (p)
+ *p = '\0';
+ return t;
+}
+
+static void print_repo(FILE *f, struct cgit_repo *repo)
+{
+ struct string_list_item *item;
+ fprintf(f, "repo.url=%s\n", repo->url);
+ fprintf(f, "repo.name=%s\n", repo->name);
+ fprintf(f, "repo.path=%s\n", repo->path);
+ if (repo->owner)
+ fprintf(f, "repo.owner=%s\n", repo->owner);
+ if (repo->desc) {
+ char *tmp = get_first_line(repo->desc);
+ fprintf(f, "repo.desc=%s\n", tmp);
+ free(tmp);
+ }
+ for_each_string_list_item(item, &repo->readme) {
+ if (item->util)
+ fprintf(f, "repo.readme=%s:%s\n", (char *)item->util, item->string);
+ else
+ fprintf(f, "repo.readme=%s\n", item->string);
+ }
+ if (repo->defbranch)
+ fprintf(f, "repo.defbranch=%s\n", repo->defbranch);
+ if (repo->extra_head_content)
+ fprintf(f, "repo.extra-head-content=%s\n", repo->extra_head_content);
+ if (repo->module_link)
+ fprintf(f, "repo.module-link=%s\n", repo->module_link);
+ if (repo->section)
+ fprintf(f, "repo.section=%s\n", repo->section);
+ if (repo->homepage)
+ fprintf(f, "repo.homepage=%s\n", repo->homepage);
+ if (repo->clone_url)
+ fprintf(f, "repo.clone-url=%s\n", repo->clone_url);
+ fprintf(f, "repo.enable-commit-graph=%d\n",
+ repo->enable_commit_graph);
+ fprintf(f, "repo.enable-log-filecount=%d\n",
+ repo->enable_log_filecount);
+ fprintf(f, "repo.enable-log-linecount=%d\n",
+ repo->enable_log_linecount);
+ if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter)
+ cgit_fprintf_filter(repo->about_filter, f, "repo.about-filter=");
+ if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter)
+ cgit_fprintf_filter(repo->commit_filter, f, "repo.commit-filter=");
+ if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter)
+ cgit_fprintf_filter(repo->source_filter, f, "repo.source-filter=");
+ if (repo->email_filter && repo->email_filter != ctx.cfg.email_filter)
+ cgit_fprintf_filter(repo->email_filter, f, "repo.email-filter=");
+ if (repo->owner_filter && repo->owner_filter != ctx.cfg.owner_filter)
+ cgit_fprintf_filter(repo->owner_filter, f, "repo.owner-filter=");
+ if (repo->snapshots != ctx.cfg.snapshots) {
+ char *tmp = build_snapshot_setting(repo->snapshots);
+ fprintf(f, "repo.snapshots=%s\n", tmp ? tmp : "");
+ free(tmp);
+ }
+ if (repo->snapshot_prefix)
+ fprintf(f, "repo.snapshot-prefix=%s\n", repo->snapshot_prefix);
+ if (repo->max_stats != ctx.cfg.max_stats)
+ fprintf(f, "repo.max-stats=%s\n",
+ cgit_find_stats_periodname(repo->max_stats));
+ if (repo->logo)
+ fprintf(f, "repo.logo=%s\n", repo->logo);
+ if (repo->logo_link)
+ fprintf(f, "repo.logo-link=%s\n", repo->logo_link);
+ fprintf(f, "repo.enable-remote-branches=%d\n", repo->enable_remote_branches);
+ fprintf(f, "repo.enable-subject-links=%d\n", repo->enable_subject_links);
+ fprintf(f, "repo.enable-html-serving=%d\n", repo->enable_html_serving);
+ if (repo->branch_sort == 1)
+ fprintf(f, "repo.branch-sort=age\n");
+ if (repo->commit_sort) {
+ if (repo->commit_sort == 1)
+ fprintf(f, "repo.commit-sort=date\n");
+ else if (repo->commit_sort == 2)
+ fprintf(f, "repo.commit-sort=topo\n");
+ }
+ fprintf(f, "repo.hide=%d\n", repo->hide);
+ fprintf(f, "repo.ignore=%d\n", repo->ignore);
+ fprintf(f, "\n");
+}
+
+static void print_repolist(FILE *f, struct cgit_repolist *list, int start)
+{
+ int i;
+
+ for (i = start; i < list->count; i++)
+ print_repo(f, &list->repos[i]);
+}
+
+/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc'
+ * and return 0 on success.
+ */
+static int generate_cached_repolist(const char *path, const char *cached_rc)
+{
+ struct strbuf locked_rc = STRBUF_INIT;
+ int result = 0;
+ int idx;
+ FILE *f;
+
+ strbuf_addf(&locked_rc, "%s.lock", cached_rc);
+ f = fopen(locked_rc.buf, "wx");
+ if (!f) {
+ /* Inform about the error unless the lockfile already existed,
+ * since that only means we've got concurrent requests.
+ */
+ result = errno;
+ if (result != EEXIST)
+ fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n",
+ locked_rc.buf, strerror(result), result);
+ goto out;
+ }
+ idx = cgit_repolist.count;
+ if (ctx.cfg.project_list)
+ scan_projects(path, ctx.cfg.project_list, repo_config);
+ else
+ scan_tree(path, repo_config);
+ print_repolist(f, &cgit_repolist, idx);
+ if (rename(locked_rc.buf, cached_rc))
+ fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n",
+ locked_rc.buf, cached_rc, strerror(errno), errno);
+ fclose(f);
+out:
+ strbuf_release(&locked_rc);
+ return result;
+}
+
+static void process_cached_repolist(const char *path)
+{
+ struct stat st;
+ struct strbuf cached_rc = STRBUF_INIT;
+ time_t age;
+ unsigned long hash;
+
+ hash = hash_str(path);
+ if (ctx.cfg.project_list)
+ hash += hash_str(ctx.cfg.project_list);
+ strbuf_addf(&cached_rc, "%s/rc-%8lx", ctx.cfg.cache_root, hash);
+
+ if (stat(cached_rc.buf, &st)) {
+ /* Nothing is cached, we need to scan without forking. And
+ * if we fail to generate a cached repolist, we need to
+ * invoke scan_tree manually.
+ */
+ if (generate_cached_repolist(path, cached_rc.buf)) {
+ if (ctx.cfg.project_list)
+ scan_projects(path, ctx.cfg.project_list,
+ repo_config);
+ else
+ scan_tree(path, repo_config);
+ }
+ goto out;
+ }
+
+ parse_configfile(cached_rc.buf, config_cb);
+
+ /* If the cached configfile hasn't expired, lets exit now */
+ age = time(NULL) - st.st_mtime;
+ if (age <= (ctx.cfg.cache_scanrc_ttl * 60))
+ goto out;
+
+ /* The cached repolist has been parsed, but it was old. So lets
+ * rescan the specified path and generate a new cached repolist
+ * in a child-process to avoid latency for the current request.
+ */
+ if (fork())
+ goto out;
+
+ exit(generate_cached_repolist(path, cached_rc.buf));
+out:
+ strbuf_release(&cached_rc);
+}
+
+static void cgit_parse_args(int argc, const char **argv)
+{
+ while (--argc > 4);
+ switch (++argc){
+ case 5:
+ ctx.env.server_port = xstrdup(argv[4]);
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- port: %s\n", ctx.env.server_port);
+#endif
+ case 4:
+ ctx.env.server_name = xstrdup(argv[3]);
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- hostname: %s\n", ctx.env.server_name);
+#endif
+ case 3:
+ if (strlen(argv[2])){
+ ctx.env.query_string = xstrdup(argv[2]);
+ ctx.qry.raw = xstrdup(argv[2]);
+ }
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- query_string: '%s'\n", ctx.env.query_string);
+#endif
+ case 2:
+ if (strlen(argv[1])){
+ ctx.env.gopher_search = xstrdup(argv[1]);
+ }
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- gopher_search: %s\n", ctx.env.gopher_search);
+#endif
+ case 1:
+ ctx.env.script_name = xstrdup(argv[0]);
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- script_name: %s\n", ctx.env.script_name);
+#endif
+ }
+
+}
+
+static int calc_ttl(void)
+{
+ if (!ctx.repo)
+ return ctx.cfg.cache_root_ttl;
+
+ if (!ctx.qry.page)
+ return ctx.cfg.cache_repo_ttl;
+
+ if (!strcmp(ctx.qry.page, "about"))
+ return ctx.cfg.cache_about_ttl;
+
+ if (!strcmp(ctx.qry.page, "snapshot"))
+ return ctx.cfg.cache_snapshot_ttl;
+
+ if (ctx.qry.has_sha1)
+ return ctx.cfg.cache_static_ttl;
+
+ if (ctx.qry.has_symref)
+ return ctx.cfg.cache_dynamic_ttl;
+
+ return ctx.cfg.cache_repo_ttl;
+}
+
+int cmd_main(int argc, const char **argv)
+{
+ /*const char *path;*/
+ int err, ttl;
+
+
+ prepare_context();
+ cgit_repolist.length = 0;
+ cgit_repolist.count = 0;
+ cgit_repolist.repos = NULL;
+
+ cgit_parse_args(argc, argv);
+ parse_configfile(expand_macros(ctx.env.cgit_config), config_cb);
+ ctx.repo = NULL;
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- cmd_main -- ctx.qry.raw: %s\n", ctx.qry.raw);
+#endif
+ http_parse_querystring(ctx.qry.raw, querystring_cb);
+
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- cmd_main -- got url: %s\n", ctx.qry.url);
+#endif
+
+ /* If virtual-root isn't specified in cgitrc, lets pretend
+ * that virtual-root equals SCRIPT_NAME, minus any possibly
+ * trailing slashes.
+ */
+ if (!ctx.cfg.virtual_root && ctx.cfg.script_name)
+ ctx.cfg.virtual_root = ensure_end(ctx.cfg.script_name, '/');
+
+ /* If no url parameter is specified on the querystring, lets
+ * use PATH_INFO as url. This allows cgit to work with virtual
+ * urls without the need for rewriterules in the webserver (as
+ * long as PATH_INFO is included in the cache lookup key).
+ */
+/* path = ctx.env.path_info;
+ if (!ctx.qry.url && path) {
+ if (path[0] == '/')
+ path++;
+ ctx.qry.url = xstrdup(path);
+ if (ctx.qry.raw) {
+ char *newqry = fmtalloc("%s?%s", path, ctx.qry.raw);
+ free(ctx.qry.raw);
+ ctx.qry.raw = newqry;
+ } else
+ ctx.qry.raw = xstrdup(ctx.qry.url);
+ cgit_parse_url(ctx.qry.url);
+ }
+*/
+ /* Before we go any further, we set ctx.env.authenticated by checking to see
+ * if the supplied cookie is valid. All cookies are valid if there is no
+ * auth_filter. If there is an auth_filter, the filter decides. */
+ /* authenticate_cookie(); */
+
+ ttl = calc_ttl();
+ if (ttl < 0)
+ ctx.page.expires += 10 * 365 * 24 * 60 * 60; /* 10 years */
+ else
+ ctx.page.expires += ttl * 60;
+ /*if (!ctx.env.authenticated || (ctx.env.request_method && * !strcmp(ctx.env.request_method, "HEAD")))*/
+ if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
+ ctx.cfg.cache_size = 0;
+ /* cache_process is the function that does the work...*/
+ err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
+ ctx.qry.raw, ttl, process_request);
+ if (err)
+ cgit_print_error("Error processing page: %s (%d)",
+ strerror(err), err);
+ return err;
+}
diff --git a/cgit.h b/cgit.h
index 32dfd7a..a482301 100644
--- a/cgit.h
+++ b/cgit.h
@@ -298,8 +298,10 @@ struct cgit_environment {
const char *http_referer;
unsigned int content_length;
int authenticated;
+ const char *gopher_search;
};
+
struct cgit_context {
struct cgit_environment env;
struct cgit_query qry;
diff --git a/cgit_70.mk b/cgit_70.mk
new file mode 100644
index 0000000..0d05ec3
--- /dev/null
+++ b/cgit_70.mk
@@ -0,0 +1,155 @@
+# This Makefile is run in the "git" directory in order to re-use Git's
+# build variables and operating system detection. Hence all files in
+# CGit's directory must be prefixed with "../".
+include Makefile
+
+CGIT_PREFIX = ../
+
+-include $(CGIT_PREFIX)cgit.conf
+
+# The CGIT_* variables are inherited when this file is called from the
+# main Makefile - they are defined there.
+
+$(CGIT_PREFIX)VERSION: force-version
+ @cd $(CGIT_PREFIX) && '$(SHELL_PATH_SQ)' ./gen-version.sh "$(CGIT_VERSION)"
+-include $(CGIT_PREFIX)VERSION
+.PHONY: force-version
+
+# CGIT_CFLAGS is a separate variable so that we can track it separately
+# and avoid rebuilding all of Git when these variables change.
+CGIT_CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"'
+CGIT_CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"'
+CGIT_CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"'
+CGIT_CFLAGS += -g
+
+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 += cgit-70.o
+CGIT_OBJ_NAMES += cache.o
+#CGIT_OBJ_NAMES += cmd.o
+CGIT_OBJ_NAMES += cmd_70.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_70-commit.o
+##CGIT_OBJ_NAMES += ui-diff.o
+CGIT_OBJ_NAMES += ui_70-diff.o
+##CGIT_OBJ_NAMES += ui-log.o
+CGIT_OBJ_NAMES += ui_70-log.o
+##CGIT_OBJ_NAMES += ui-patch.o
+CGIT_OBJ_NAMES += ui_70-patch.o
+CGIT_OBJ_NAMES += ui-plain.o
+##CGIT_OBJ_NAMES += ui-refs.o
+CGIT_OBJ_NAMES += ui_70-refs.o
+##CGIT_OBJ_NAMES += ui-repolist.o
+CGIT_OBJ_NAMES += ui_70-repolist.o
+##CGIT_OBJ_NAMES += ui-shared.o
+CGIT_OBJ_NAMES += ui_70-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_70-summary.o
+##CGIT_OBJ_NAMES += ui-tag.o
+CGIT_OBJ_NAMES += ui_70-tag.o
+##CGIT_OBJ_NAMES += ui-tree.o
+CGIT_OBJ_NAMES += ui_70-tree.o
+
+CGIT_OBJS := $(addprefix $(CGIT_PREFIX),$(CGIT_OBJ_NAMES))
+
+# Only cgit.c reference CGIT_VERSION so we only rebuild its objects when the
+# version changes.
+##CGIT_VERSION_OBJS := $(addprefix $(CGIT_PREFIX),cgit.o cgit.sp)
+CGIT_VERSION_OBJS := $(addprefix $(CGIT_PREFIX),cgit-70.o cgit.sp)
+$(CGIT_VERSION_OBJS): $(CGIT_PREFIX)VERSION
+$(CGIT_VERSION_OBJS): EXTRA_CPPFLAGS = \
+ -DCGIT_VERSION='"$(CGIT_VERSION)"'
+
+# Git handles dependencies using ":=" so dependencies in CGIT_OBJ 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_PREFIX).depend),)
+missing_dep_dirs += $(CGIT_PREFIX).depend
+endif
+
+$(CGIT_PREFIX).depend:
+ @mkdir -p $@
+
+$(CGIT_PREFIX)CGIT-CFLAGS: FORCE
+ @FLAGS='$(subst ','\'',$(CGIT_CFLAGS))'; \
+ if test x"$$FLAGS" != x"`cat ../CGIT-CFLAGS 2>/dev/null`" ; then \
+ echo 1>&2 " * new CGit build flags"; \
+ echo "$$FLAGS" >$(CGIT_PREFIX)CGIT-CFLAGS; \
+ fi
+
+$(CGIT_OBJS): %.o: %.c GIT-CFLAGS $(CGIT_PREFIX)CGIT-CFLAGS $(missing_dep_dirs)
+ $(QUIET_CC)$(CC) -o $*.o -c $(dep_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $(CGIT_CFLAGS) $<
+
+$(CGIT_PREFIX)cgit-70: $(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): %.sp: %.c GIT-CFLAGS $(CGIT_PREFIX)CGIT-CFLAGS FORCE
+ $(QUIET_SP)cgcc -no-compile $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $(CGIT_CFLAGS) $(SPARSE_FLAGS) $<
+
+cgit-sparse: $(CGIT_SP_OBJS)
diff --git a/cmd_70.c b/cmd_70.c
new file mode 100644
index 0000000..7e33c0b
--- /dev/null
+++ b/cmd_70.c
@@ -0,0 +1,234 @@
+/* cmd.c: the cgit command dispatcher
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "cmd.h"
+#include "cache.h"
+#include "ui_70-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_gopher_error("Not implemented");
+ return;
+/* cgit_clone_head();*/
+}
+
+static void atom_fn(void)
+{
+ cgit_gopher_error("Not implemented");
+ return;
+/* cgit_print_atom(ctx.qry.head, ctx.qry.path, ctx.cfg.max_atom_items);*/
+}
+
+static void about_fn(void)
+{
+ cgit_gopher_error("Not implemented");
+ return;
+/*
+ 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)
+{
+ cgit_gopher_error("Not implemented");
+ return;
+/*
+ if (ctx.cfg.enable_blame)
+ cgit_print_blame();
+ else
+ cgit_print_error_page(403, "Forbidden", "Blame is disabled");
+*/
+}
+
+static void blob_fn(void)
+{
+ cgit_gopher_error("Not implemented");
+ return;
+/* cgit_print_blob(ctx.qry.sha1, ctx.qry.path, ctx.qry.head, 0);*/
+}
+
+static void commit_fn(void)
+{
+ cgit_print_commit(ctx.qry.sha1, ctx.qry.path);
+}
+
+static void diff_fn(void)
+{
+ cgit_print_diff(ctx.qry.sha1, ctx.qry.sha2, ctx.qry.path, 1, 0);
+}
+
+static void rawdiff_fn(void)
+{
+ cgit_print_diff(ctx.qry.sha1, ctx.qry.sha2, ctx.qry.path, 1, 1);
+}
+
+static void info_fn(void)
+{
+ cgit_gopher_error("Not implemented");
+ return;
+/* cgit_clone_info();*/
+}
+
+static void log_fn(void)
+{
+ cgit_print_log(ctx.qry.sha1, 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_gopher_error("Not implemented");
+ return;
+/* cgit_clone_objects();*/
+}
+
+static void repolist_fn(void)
+{
+ cgit_print_repolist();
+}
+
+static void patch_fn(void)
+{
+ cgit_print_patch(ctx.qry.sha1, ctx.qry.sha2, 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_gopher_error("Not implemented");
+ return;
+/* cgit_print_snapshot(ctx.qry.head, ctx.qry.sha1, ctx.qry.path, ctx.qry.nohead);*/
+}
+
+static void stats_fn(void)
+{
+ cgit_gopher_error("Not implemented");
+ return;
+/* cgit_show_stats();*/
+}
+
+static void summary_fn(void)
+{
+#ifdef DEBUG_GOPHER
+ fprintf(stderr, " ---- selected function: cgit_print_summary\n");
+#endif
+ cgit_print_summary();
+}
+
+static void tag_fn(void)
+{
+ cgit_print_tag(ctx.qry.sha1);
+}
+
+static void tree_fn(void)
+{
+ cgit_print_tree(ctx.qry.sha1, 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/shared_70.c b/shared_70.c
new file mode 100644
index 0000000..609bd2a
--- /dev/null
+++ b/shared_70.c
@@ -0,0 +1,578 @@
+/* shared.c: global vars + some callback 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"
+
+struct cgit_repolist cgit_repolist;
+struct cgit_context ctx;
+
+int chk_zero(int result, char *msg)
+{
+ if (result != 0)
+ die_errno("%s", msg);
+ return result;
+}
+
+int chk_positive(int result, char *msg)
+{
+ if (result <= 0)
+ die_errno("%s", msg);
+ return result;
+}
+
+int chk_non_negative(int result, char *msg)
+{
+ if (result < 0)
+ die_errno("%s", msg);
+ return result;
+}
+
+char *cgit_default_repo_desc = "[no description]";
+struct cgit_repo *cgit_add_repo(const char *url)
+{
+ struct cgit_repo *ret;
+
+ if (++cgit_repolist.count > cgit_repolist.length) {
+ if (cgit_repolist.length == 0)
+ cgit_repolist.length = 8;
+ else
+ cgit_repolist.length *= 2;
+ cgit_repolist.repos = xrealloc(cgit_repolist.repos,
+ cgit_repolist.length *
+ sizeof(struct cgit_repo));
+ }
+
+ ret = &cgit_repolist.repos[cgit_repolist.count-1];
+ memset(ret, 0, sizeof(struct cgit_repo));
+ ret->url = trim_end(url, '/');
+ ret->name = ret->url;
+ ret->path = NULL;
+ ret->desc = cgit_default_repo_desc;
+ ret->extra_head_content = NULL;
+ ret->owner = NULL;
+ ret->homepage = NULL;
+ ret->section = ctx.cfg.section;
+ ret->snapshots = ctx.cfg.snapshots;
+ ret->enable_commit_graph = ctx.cfg.enable_commit_graph;
+ ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
+ ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
+ ret->enable_remote_branches = ctx.cfg.enable_remote_branches;
+ ret->enable_subject_links = ctx.cfg.enable_subject_links;
+ ret->enable_html_serving = ctx.cfg.enable_html_serving;
+ ret->max_stats = ctx.cfg.max_stats;
+ ret->branch_sort = ctx.cfg.branch_sort;
+ ret->commit_sort = ctx.cfg.commit_sort;
+ ret->module_link = ctx.cfg.module_link;
+ ret->readme = ctx.cfg.readme;
+ ret->mtime = -1;
+ ret->about_filter = ctx.cfg.about_filter;
+ ret->commit_filter = ctx.cfg.commit_filter;
+ ret->source_filter = ctx.cfg.source_filter;
+ ret->email_filter = ctx.cfg.email_filter;
+ ret->owner_filter = ctx.cfg.owner_filter;
+ ret->clone_url = ctx.cfg.clone_url;
+ ret->submodules.strdup_strings = 1;
+ ret->hide = ret->ignore = 0;
+ return ret;
+}
+
+struct cgit_repo *cgit_get_repoinfo(const char *url)
+{
+ int i;
+ struct cgit_repo *repo;
+
+ for (i = 0; i < cgit_repolist.count; i++) {
+ repo = &cgit_repolist.repos[i];
+ if (repo->ignore)
+ continue;
+ if (!strcmp(repo->url, url))
+ return repo;
+ }
+ return NULL;
+}
+
+void cgit_free_commitinfo(struct commitinfo *info)
+{
+ free(info->author);
+ free(info->author_email);
+ free(info->committer);
+ free(info->committer_email);
+ free(info->subject);
+ free(info->msg);
+ free(info->msg_encoding);
+ free(info);
+}
+
+char *trim_end(const char *str, char c)
+{
+ int len;
+
+ if (str == NULL)
+ return NULL;
+ len = strlen(str);
+ while (len > 0 && str[len - 1] == c)
+ len--;
+ if (len == 0)
+ return NULL;
+ return xstrndup(str, len);
+}
+
+char *ensure_end(const char *str, char c)
+{
+ size_t len = strlen(str);
+ char *result;
+
+ if (len && str[len - 1] == c)
+ return xstrndup(str, len);
+
+ result = xmalloc(len + 2);
+ memcpy(result, str, len);
+ result[len] = '/';
+ result[len + 1] = '\0';
+ return result;
+}
+
+void strbuf_ensure_end(struct strbuf *sb, char c)
+{
+ if (!sb->len || sb->buf[sb->len - 1] != c)
+ strbuf_addch(sb, c);
+}
+
+void cgit_add_ref(struct reflist *list, struct refinfo *ref)
+{
+ size_t size;
+
+ if (list->count >= list->alloc) {
+ list->alloc += (list->alloc ? list->alloc : 4);
+ size = list->alloc * sizeof(struct refinfo *);
+ list->refs = xrealloc(list->refs, size);
+ }
+ list->refs[list->count++] = ref;
+}
+
+static struct refinfo *cgit_mk_refinfo(const char *refname, const struct object_id *oid)
+{
+ struct refinfo *ref;
+
+ ref = xmalloc(sizeof (struct refinfo));
+ ref->refname = xstrdup(refname);
+ ref->object = parse_object(oid);
+ switch (ref->object->type) {
+ case OBJ_TAG:
+ ref->tag = cgit_parse_tag((struct tag *)ref->object);
+ break;
+ case OBJ_COMMIT:
+ ref->commit = cgit_parse_commit((struct commit *)ref->object);
+ break;
+ }
+ return ref;
+}
+
+void cgit_free_taginfo(struct taginfo *tag)
+{
+ if (tag->tagger)
+ free(tag->tagger);
+ if (tag->tagger_email)
+ free(tag->tagger_email);
+ if (tag->msg)
+ free(tag->msg);
+ free(tag);
+}
+
+static void cgit_free_refinfo(struct refinfo *ref)
+{
+ if (ref->refname)
+ free((char *)ref->refname);
+ switch (ref->object->type) {
+ case OBJ_TAG:
+ cgit_free_taginfo(ref->tag);
+ break;
+ case OBJ_COMMIT:
+ cgit_free_commitinfo(ref->commit);
+ break;
+ }
+ free(ref);
+}
+
+void cgit_free_reflist_inner(struct reflist *list)
+{
+ int i;
+
+ for (i = 0; i < list->count; i++) {
+ cgit_free_refinfo(list->refs[i]);
+ }
+ free(list->refs);
+}
+
+int cgit_refs_cb(const char *refname, const struct object_id *oid, int flags,
+ void *cb_data)
+{
+ struct reflist *list = (struct reflist *)cb_data;
+ struct refinfo *info = cgit_mk_refinfo(refname, oid);
+
+ if (info)
+ cgit_add_ref(list, info);
+ return 0;
+}
+
+void cgit_diff_tree_cb(struct diff_queue_struct *q,
+ struct diff_options *options, void *data)
+{
+ int i;
+
+ for (i = 0; i < q->nr; i++) {
+ if (q->queue[i]->status == 'U')
+ continue;
+ ((filepair_fn)data)(q->queue[i]);
+ }
+}
+
+static int load_mmfile(mmfile_t *file, const struct object_id *oid)
+{
+ enum object_type type;
+
+ if (is_null_oid(oid)) {
+ file->ptr = (char *)"";
+ file->size = 0;
+ } else {
+ file->ptr = read_object_file(oid, &type,
+ (unsigned long *)&file->size);
+ }
+ return 1;
+}
+
+/*
+ * Receive diff-buffers from xdiff and concatenate them as
+ * needed across multiple callbacks.
+ *
+ * This is basically a copy of xdiff-interface.c/xdiff_outf(),
+ * ripped from git and modified to use globals instead of
+ * a special callback-struct.
+ */
+static char *diffbuf = NULL;
+static int buflen = 0;
+
+static int filediff_cb(void *priv, mmbuffer_t *mb, int nbuf)
+{
+ int i;
+
+ for (i = 0; i < nbuf; i++) {
+ if (mb[i].ptr[mb[i].size-1] != '\n') {
+ /* Incomplete line */
+ diffbuf = xrealloc(diffbuf, buflen + mb[i].size);
+ memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size);
+ buflen += mb[i].size;
+ continue;
+ }
+
+ /* we have a complete line */
+ if (!diffbuf) {
+ ((linediff_fn)priv)(mb[i].ptr, mb[i].size);
+ continue;
+ }
+ diffbuf = xrealloc(diffbuf, buflen + mb[i].size);
+ memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size);
+ ((linediff_fn)priv)(diffbuf, buflen + mb[i].size);
+ free(diffbuf);
+ diffbuf = NULL;
+ buflen = 0;
+ }
+ if (diffbuf) {
+ ((linediff_fn)priv)(diffbuf, buflen);
+ free(diffbuf);
+ diffbuf = NULL;
+ buflen = 0;
+ }
+ return 0;
+}
+
+int cgit_diff_files(const struct object_id *old_oid,
+ const struct object_id *new_oid, unsigned long *old_size,
+ unsigned long *new_size, int *binary, int context,
+ int ignorews, linediff_fn fn)
+{
+ mmfile_t file1, file2;
+ xpparam_t diff_params;
+ xdemitconf_t emit_params;
+ xdemitcb_t emit_cb;
+
+ if (!load_mmfile(&file1, old_oid) || !load_mmfile(&file2, new_oid))
+ return 1;
+
+ *old_size = file1.size;
+ *new_size = file2.size;
+
+ if ((file1.ptr && buffer_is_binary(file1.ptr, file1.size)) ||
+ (file2.ptr && buffer_is_binary(file2.ptr, file2.size))) {
+ *binary = 1;
+ if (file1.size)
+ free(file1.ptr);
+ if (file2.size)
+ free(file2.ptr);
+ return 0;
+ }
+
+ memset(&diff_params, 0, sizeof(diff_params));
+ memset(&emit_params, 0, sizeof(emit_params));
+ memset(&emit_cb, 0, sizeof(emit_cb));
+ diff_params.flags = XDF_NEED_MINIMAL;
+ if (ignorews)
+ diff_params.flags |= XDF_IGNORE_WHITESPACE;
+ emit_params.ctxlen = context > 0 ? context : 3;
+ emit_params.flags = XDL_EMIT_FUNCNAMES;
+ emit_cb.outf = filediff_cb;
+ emit_cb.priv = fn;
+ xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb);
+ if (file1.size)
+ free(file1.ptr);
+ if (file2.size)
+ free(file2.ptr);
+ return 0;
+}
+
+void cgit_diff_tree(const struct object_id *old_oid,
+ const struct object_id *new_oid,
+ filepair_fn fn, const char *prefix, int ignorews)
+{
+ struct diff_options opt;
+ struct pathspec_item item;
+
+ memset(&item, 0, sizeof(item));
+ diff_setup(&opt);
+ opt.output_format = DIFF_FORMAT_CALLBACK;
+ opt.detect_rename = 1;
+ opt.rename_limit = ctx.cfg.renamelimit;
+ opt.flags.recursive = 1;
+ if (ignorews)
+ DIFF_XDL_SET(&opt, IGNORE_WHITESPACE);
+ opt.format_callback = cgit_diff_tree_cb;
+ opt.format_callback_data = fn;
+ if (prefix) {
+ item.match = xstrdup(prefix);
+ item.len = strlen(prefix);
+ opt.pathspec.nr = 1;
+ opt.pathspec.items = &item;
+ }
+ diff_setup_done(&opt);
+
+ if (old_oid && !is_null_oid(old_oid))
+ diff_tree_oid(old_oid, new_oid, "", &opt);
+ else
+ diff_root_tree_oid(new_oid, "", &opt);
+ diffcore_std(&opt);
+ diff_flush(&opt);
+
+ free(item.match);
+}
+
+void cgit_diff_commit(struct commit *commit, filepair_fn fn, const char *prefix)
+{
+ const struct object_id *old_oid = NULL;
+
+ if (commit->parents)
+ old_oid = &commit->parents->item->object.oid;
+ cgit_diff_tree(old_oid, &commit->object.oid, fn, prefix,
+ ctx.qry.ignorews);
+}
+
+int cgit_parse_snapshots_mask(const char *str)
+{
+ struct string_list tokens = STRING_LIST_INIT_DUP;
+ struct string_list_item *item;
+ const struct cgit_snapshot_format *f;
+ int rv = 0;
+
+ /* favor legacy setting */
+ if (atoi(str))
+ return 1;
+
+ if (strcmp(str, "all") == 0)
+ return INT_MAX;
+
+ string_list_split(&tokens, str, ' ', -1);
+ string_list_remove_empty_items(&tokens, 0);
+
+ for_each_string_list_item(item, &tokens) {
+ for (f = cgit_snapshot_formats; f->suffix; f++) {
+ if (!strcmp(item->string, f->suffix) ||
+ !strcmp(item->string, f->suffix + 1)) {
+ rv |= cgit_snapshot_format_bit(f);
+ break;
+ }
+ }
+ }
+
+ string_list_clear(&tokens, 0);
+ return rv;
+}
+
+typedef struct {
+ char * name;
+ char * value;
+} cgit_env_var;
+
+void cgit_prepare_repo_env(struct cgit_repo * repo)
+{
+ cgit_env_var env_vars[] = {
+ { .name = "CGIT_REPO_URL", .value = repo->url },
+ { .name = "CGIT_REPO_NAME", .value = repo->name },
+ { .name = "CGIT_REPO_PATH", .value = repo->path },
+ { .name = "CGIT_REPO_OWNER", .value = repo->owner },
+ { .name = "CGIT_REPO_DEFBRANCH", .value = repo->defbranch },
+ { .name = "CGIT_REPO_SECTION", .value = repo->section },
+ { .name = "CGIT_REPO_CLONE_URL", .value = repo->clone_url }
+ };
+ int env_var_count = ARRAY_SIZE(env_vars);
+ cgit_env_var *p, *q;
+ static char *warn = "cgit warning: failed to set env: %s=%s\n";
+
+ p = env_vars;
+ q = p + env_var_count;
+ for (; p < q; p++)
+ if (p->value && setenv(p->name, p->value, 1))
+ fprintf(stderr, warn, p->name, p->value);
+}
+
+/* Read the content of the specified file into a newly allocated buffer,
+ * zeroterminate the buffer and return 0 on success, errno otherwise.
+ */
+int readfile(const char *path, char **buf, size_t *size)
+{
+ int fd, e;
+ struct stat st;
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1)
+ return errno;
+ if (fstat(fd, &st)) {
+ e = errno;
+ close(fd);
+ return e;
+ }
+ if (!S_ISREG(st.st_mode)) {
+ close(fd);
+ return EISDIR;
+ }
+ *buf = xmalloc(st.st_size + 1);
+ *size = read_in_full(fd, *buf, st.st_size);
+ e = errno;
+ (*buf)[*size] = '\0';
+ close(fd);
+ return (*size == st.st_size ? 0 : e);
+}
+
+static int is_token_char(char c)
+{
+ return isalnum(c) || c == '_';
+}
+
+/* Replace name with getenv(name), return pointer to zero-terminating char
+ */
+static char *expand_macro(char *name, int maxlength)
+{
+ char *value;
+ size_t len;
+
+ len = 0;
+ value = getenv(name);
+ if (value) {
+ len = strlen(value) + 1;
+ if (len > maxlength)
+ len = maxlength;
+ strlcpy(name, value, len);
+ --len;
+ }
+ return name + len;
+}
+
+#define EXPBUFSIZE (1024 * 8)
+
+/* Replace all tokens prefixed by '$' in the specified text with the
+ * value of the named environment variable.
+ * NB: the return value is a static buffer, i.e. it must be strdup'd
+ * by the caller.
+ */
+char *expand_macros(const char *txt)
+{
+ static char result[EXPBUFSIZE];
+ char *p, *start;
+ int len;
+
+ p = result;
+ start = NULL;
+ while (p < result + EXPBUFSIZE - 1 && txt && *txt) {
+ *p = *txt;
+ if (start) {
+ if (!is_token_char(*txt)) {
+ if (p - start > 0) {
+ *p = '\0';
+ len = result + EXPBUFSIZE - start - 1;
+ p = expand_macro(start, len) - 1;
+ }
+ start = NULL;
+ txt--;
+ }
+ p++;
+ txt++;
+ continue;
+ }
+ if (*txt == '$') {
+ start = p;
+ txt++;
+ continue;
+ }
+ p++;
+ txt++;
+ }
+ *p = '\0';
+ if (start && p - start > 0) {
+ len = result + EXPBUFSIZE - start - 1;
+ p = expand_macro(start, len);
+ *p = '\0';
+ }
+ return result;
+}
+
+char *get_mimetype_for_filename(const char *filename)
+{
+ char *ext, *mimetype, *token, line[1024], *saveptr;
+ FILE *file;
+ struct string_list_item *mime;
+
+ if (!filename)
+ return NULL;
+
+ ext = strrchr(filename, '.');
+ if (!ext)
+ return NULL;
+ ++ext;
+ if (!ext[0])
+ return NULL;
+ mime = string_list_lookup(&ctx.cfg.mimetypes, ext);
+ if (mime)
+ return xstrdup(mime->util);
+
+ if (!ctx.cfg.mimetype_file)
+ return NULL;
+ file = fopen(ctx.cfg.mimetype_file, "r");
+ if (!file)
+ return NULL;
+ while (fgets(line, sizeof(line), file)) {
+ if (!line[0] || line[0] == '#')
+ continue;
+ mimetype = strtok_r(line, " \t\r\n", &saveptr);
+ while ((token = strtok_r(NULL, " \t\r\n", &saveptr))) {
+ if (!strcasecmp(ext, token)) {
+ fclose(file);
+ return xstrdup(mimetype);
+ }
+ }
+ }
+ fclose(file);
+ return NULL;
+}
diff --git a/ui-shared_70.c b/ui-shared_70.c
new file mode 100644
index 0000000..54ea099
--- /dev/null
+++ b/ui-shared_70.c
@@ -0,0 +1,40 @@
+/*
+*
+* Gopher-related functions
+*
+*/
+
+#include <stdio.h>
+
+void cgit_gopher_selector(char type, char *str, char *sel, char *host, int port){
+
+ printf("%c%s\t%s\t%s\t%d\r\n",
+ type, str, sel, host, port);
+}
+
+
+void cgit_gopher_info(char *msg, char *host, int port){
+ cgit_gopher_selector('i', msg, "", host, port);
+}
+
+void cgit_gopher_menu(char *descr, char *sel, char *host, int port){
+
+ cgit_gopher_selector('1', descr, sel, host, port);
+}
+
+void cgit_gopher_textfile(char *descr, char *sel, char *host, int port){
+
+ cgit_gopher_selector('0', descr, sel, host, port);
+}
+
+void cgit_gopher_error(char *msg, char *host, int port){
+ cgit_gopher_selector('3', msg, "Err", host, port);
+}
+
+void cgit_tree_link_gopher(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+
+
+
+}
diff --git a/ui-shared_70.h b/ui-shared_70.h
new file mode 100644
index 0000000..3c5ef55
--- /dev/null
+++ b/ui-shared_70.h
@@ -0,0 +1,13 @@
+#ifndef GOPHER_H
+#define GOPHER_H
+
+#define CGIT_GOPHER_HOST "localhost"
+#define CGIT_GOPHER_PORT 1500
+
+void cgit_gopher_selector(char type, char *str, char *sel, char *host, int port);
+void cgit_gopher_info(char *msg, char *host, int port);
+void cgit_gopher_menu(char *descr, char *sel, char *host, int port);
+void cgit_gopher_textfile(char *descr, char *sel, char *host, int port);
+void cgit_gopher_error(char *descr, char *host, int port);
+
+#endif /* GOPHER_H */
diff --git a/ui_70-commit.c b/ui_70-commit.c
new file mode 100644
index 0000000..d7b5f9d
--- /dev/null
+++ b/ui_70-commit.c
@@ -0,0 +1,171 @@
+/* ui-commit.c: generate commit view
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-commit.h"
+#include "html.h"
+#include "ui_70-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 (get_oid(hex, &oid)) {
+ cgit_gopher_error("Bad object id");
+ return;
+ }
+ commit = lookup_commit_reference(&oid);
+ if (!commit) {
+ cgit_gopher_error("Bad commit reference");
+ return;
+ }
+ info = cgit_parse_commit(commit);
+
+ /*format_display_notes(&oid, &notes, PAGE_ENCODING, 0);*/
+
+ load_ref_decorations(NULL, DECORATE_FULL_REFS);
+
+ cgit_print_layout_start();
+ /*cgit_print_diff_ctrls();*/
+
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text("author: ");
+ cgit_gopher_text(info->author);
+ if (!ctx.cfg.noplainemail) {
+ cgit_gopher_text(" ");
+ cgit_gopher_text(info->author_email);
+ cgit_gopher_text(" ");
+ }
+ cgit_gopher_text(show_date(info->author_date, info->author_tz,
+ cgit_date_mode(DATE_ISO8601)));
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text("committer: ");
+ cgit_gopher_text(info->committer);
+ if (!ctx.cfg.noplainemail) {
+ cgit_gopher_text(" ");
+ cgit_gopher_text(info->committer_email);
+ cgit_gopher_text(" ");
+ }
+ cgit_gopher_text(show_date(info->committer_date, info->committer_tz,
+ cgit_date_mode(DATE_ISO8601)));
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text("commit ");
+ tmp = oid_to_hex(&commit->object.oid);
+ cgit_gopher_text(tmp);
+ cgit_gopher_tab();
+ cgit_commit_link(tmp, NULL, NULL, ctx.qry.head, tmp, prefix);
+ cgit_gopher_end_selector();
+
+ cgit_gopher_start_selector(GOPHER_TXT);
+ cgit_gopher_text("patch ");
+ cgit_gopher_tab();
+ cgit_patch_link("patch", NULL, NULL, NULL, tmp, prefix);
+ cgit_gopher_end_selector();
+
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text("tree ");
+ cgit_gopher_tab();
+ tmp = xstrdup(hex);
+ cgit_tree_link(oid_to_hex(&commit->maybe_tree->object.oid), NULL, NULL,
+ ctx.qry.head, tmp, NULL);
+ cgit_gopher_end_selector();
+
+/* if (prefix) {
+ html(" /");
+ cgit_tree_link(prefix, NULL, NULL, ctx.qry.head, tmp, prefix);
+ }
+*/ free(tmp);
+
+ for (p = commit->parents; p; p = p->next) {
+ parent = lookup_commit_reference(&p->item->object.oid);
+ if (!parent) {
+ cgit_gopher_info("Error reading parent commit");
+ continue;
+ }
+
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text("parent ");
+ cgit_gopher_tab();
+ 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);
+ cgit_gopher_end_selector();
+ /*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='sha1'>");
+ cgit_print_snapshot_links(ctx.repo, hex, "<br/>");
+ html("</td></tr>");
+ }
+ */
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text("subject: ");
+ cgit_gopher_text(info->subject);
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+
+ if (strlen(info->msg)){
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text("message: ");
+ cgit_gopher_text(info->msg);
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+ }
+
+ /* FIXME: NOTES HAVE BEEN DISABLED*/
+/* 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.sha1, tmp, prefix, 0, 0);
+ }
+*/
+ strbuf_release(&notes);
+ cgit_free_commitinfo(info);
+ cgit_print_layout_end();
+}
diff --git a/ui_70-diff.c b/ui_70-diff.c
new file mode 100644
index 0000000..2fe52b9
--- /dev/null
+++ b/ui_70-diff.c
@@ -0,0 +1,518 @@
+/* ui-diff.c: show diff between two blobs
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-diff.h"
+#include "html.h"
+#include "ui_70-shared.h"
+#include "ui-ssdiff.h"
+
+struct object_id old_rev_oid[1];
+struct object_id new_rev_oid[1];
+
+static int files, slots;
+static int total_adds, total_rems, max_changes;
+static int lines_added, lines_removed;
+
+static struct fileinfo {
+ char status;
+ struct object_id old_oid[1];
+ struct object_id new_oid[1];
+ unsigned short old_mode;
+ unsigned short new_mode;
+ char *old_path;
+ char *new_path;
+ unsigned int added;
+ unsigned int removed;
+ unsigned long old_size;
+ unsigned long new_size;
+ unsigned int binary:1;
+} *items;
+
+static int use_ssdiff = 0;
+static struct diff_filepair *current_filepair;
+static const char *current_prefix;
+
+struct diff_filespec *cgit_get_current_old_file(void)
+{
+ return current_filepair->one;
+}
+
+struct diff_filespec *cgit_get_current_new_file(void)
+{
+ return current_filepair->two;
+}
+
+static void print_fileinfo(struct fileinfo *info)
+{
+ char *class;
+
+ switch (info->status) {
+ case DIFF_STATUS_ADDED:
+ class = "add";
+ break;
+ case DIFF_STATUS_COPIED:
+ class = "cpy";
+ break;
+ case DIFF_STATUS_DELETED:
+ class = "del";
+ break;
+ case DIFF_STATUS_MODIFIED:
+ class = "upd";
+ break;
+ case DIFF_STATUS_RENAMED:
+ class = "mov";
+ break;
+ case DIFF_STATUS_TYPE_CHANGED:
+ class = "typ";
+ break;
+ case DIFF_STATUS_UNKNOWN:
+ class = "unk";
+ break;
+ case DIFF_STATUS_UNMERGED:
+ class = "stg";
+ break;
+ default:
+ die("bug: unhandled diff status %c", info->status);
+ }
+
+ html("<tr>");
+ htmlf("<td class='mode'>");
+ if (is_null_oid(info->new_oid)) {
+ cgit_print_filemode(info->old_mode);
+ } else {
+ cgit_print_filemode(info->new_mode);
+ }
+
+ if (info->old_mode != info->new_mode &&
+ !is_null_oid(info->old_oid) &&
+ !is_null_oid(info->new_oid)) {
+ html("<span class='modechange'>[");
+ cgit_print_filemode(info->old_mode);
+ html("]</span>");
+ }
+ htmlf("</td><td class='%s'>", class);
+ cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.sha1,
+ ctx.qry.sha2, info->new_path);
+ if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED) {
+ htmlf(" (%s from ",
+ info->status == DIFF_STATUS_COPIED ? "copied" : "renamed");
+ html_txt(info->old_path);
+ html(")");
+ }
+ html("</td><td class='right'>");
+ if (info->binary) {
+ htmlf("bin</td><td class='graph'>%ld -> %ld bytes",
+ info->old_size, info->new_size);
+ return;
+ }
+ htmlf("%d", info->added + info->removed);
+ html("</td><td class='graph'>");
+ htmlf("<table summary='file diffstat' width='%d%%'><tr>", (max_changes > 100 ? 100 : max_changes));
+ htmlf("<td class='add' style='width: %.1f%%;'/>",
+ info->added * 100.0 / max_changes);
+ htmlf("<td class='rem' style='width: %.1f%%;'/>",
+ info->removed * 100.0 / max_changes);
+ htmlf("<td class='none' style='width: %.1f%%;'/>",
+ (max_changes - info->removed - info->added) * 100.0 / max_changes);
+ html("</tr></table></td></tr>\n");
+}
+
+static void count_diff_lines(char *line, int len)
+{
+ if (line && (len > 0)) {
+ if (line[0] == '+')
+ lines_added++;
+ else if (line[0] == '-')
+ lines_removed++;
+ }
+}
+
+static int show_filepair(struct diff_filepair *pair)
+{
+ /* Always show if we have no limiting prefix. */
+ if (!current_prefix)
+ return 1;
+
+ /* Show if either path in the pair begins with the prefix. */
+ if (starts_with(pair->one->path, current_prefix) ||
+ starts_with(pair->two->path, current_prefix))
+ return 1;
+
+ /* Otherwise we don't want to show this filepair. */
+ return 0;
+}
+
+static void inspect_filepair(struct diff_filepair *pair)
+{
+ int binary = 0;
+ unsigned long old_size = 0;
+ unsigned long new_size = 0;
+
+ if (!show_filepair(pair))
+ return;
+
+ files++;
+ lines_added = 0;
+ lines_removed = 0;
+ cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size, &new_size,
+ &binary, 0, ctx.qry.ignorews, count_diff_lines);
+ if (files >= slots) {
+ if (slots == 0)
+ slots = 4;
+ else
+ slots = slots * 2;
+ items = xrealloc(items, slots * sizeof(struct fileinfo));
+ }
+ items[files-1].status = pair->status;
+ oidcpy(items[files-1].old_oid, &pair->one->oid);
+ oidcpy(items[files-1].new_oid, &pair->two->oid);
+ items[files-1].old_mode = pair->one->mode;
+ items[files-1].new_mode = pair->two->mode;
+ items[files-1].old_path = xstrdup(pair->one->path);
+ items[files-1].new_path = xstrdup(pair->two->path);
+ items[files-1].added = lines_added;
+ items[files-1].removed = lines_removed;
+ items[files-1].old_size = old_size;
+ items[files-1].new_size = new_size;
+ items[files-1].binary = binary;
+ if (lines_added + lines_removed > max_changes)
+ max_changes = lines_added + lines_removed;
+ total_adds += lines_added;
+ total_rems += lines_removed;
+}
+
+static void cgit_print_diffstat(const struct object_id *old_oid,
+ const struct object_id *new_oid,
+ const char *prefix)
+{
+ int i;
+
+ html("<div class='diffstat-header'>");
+ cgit_diff_link("Diffstat", NULL, NULL, ctx.qry.head, ctx.qry.sha1,
+ ctx.qry.sha2, NULL);
+ if (prefix) {
+ html(" (limited to '");
+ html_txt(prefix);
+ html("')");
+ }
+ html("</div>");
+ html("<table summary='diffstat' class='diffstat'>");
+ max_changes = 0;
+ cgit_diff_tree(old_oid, new_oid, inspect_filepair, prefix,
+ ctx.qry.ignorews);
+ for (i = 0; i<files; i++)
+ print_fileinfo(&items[i]);
+ html("</table>");
+ html("<div class='diffstat-summary'>");
+ htmlf("%d files changed, %d insertions, %d deletions",
+ files, total_adds, total_rems);
+ html("</div>");
+}
+
+
+/*
+ * print a single line returned from xdiff
+ */
+static void print_line(char *line, int len)
+{
+ char *class = "ctx";
+ char c = line[len-1];
+
+ if (line[0] == '+')
+ class = "add";
+ else if (line[0] == '-')
+ class = "del";
+ else if (line[0] == '@')
+ class = "hunk";
+
+ htmlf("<div class='%s'>", class);
+ line[len-1] = '\0';
+ html_txt(line);
+ html("</div>");
+ line[len-1] = c;
+}
+
+static void header(const struct object_id *oid1, char *path1, int mode1,
+ const struct object_id *oid2, char *path2, int mode2)
+{
+ char *abbrev1, *abbrev2;
+ int subproject;
+
+ subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2));
+ html("<div class='head'>");
+ html("diff --git a/");
+ html_txt(path1);
+ html(" b/");
+ html_txt(path2);
+
+ if (mode1 == 0)
+ htmlf("<br/>new file mode %.6o", mode2);
+
+ if (mode2 == 0)
+ htmlf("<br/>deleted file mode %.6o", mode1);
+
+ if (!subproject) {
+ abbrev1 = xstrdup(find_unique_abbrev(oid1, DEFAULT_ABBREV));
+ abbrev2 = xstrdup(find_unique_abbrev(oid2, DEFAULT_ABBREV));
+ htmlf("<br/>index %s..%s", abbrev1, abbrev2);
+ free(abbrev1);
+ free(abbrev2);
+ if (mode1 != 0 && mode2 != 0) {
+ htmlf(" %.6o", mode1);
+ if (mode2 != mode1)
+ htmlf("..%.6o", mode2);
+ }
+ if (is_null_oid(oid1)) {
+ path1 = "dev/null";
+ html("<br/>--- /");
+ } else
+ html("<br/>--- a/");
+ if (mode1 != 0)
+ cgit_tree_link(path1, NULL, NULL, ctx.qry.head,
+ oid_to_hex(old_rev_oid), path1);
+ else
+ html_txt(path1);
+ if (is_null_oid(oid2)) {
+ path2 = "dev/null";
+ html("<br/>+++ /");
+ } else
+ html("<br/>+++ b/");
+ if (mode2 != 0)
+ cgit_tree_link(path2, NULL, NULL, ctx.qry.head,
+ oid_to_hex(new_rev_oid), path2);
+ else
+ html_txt(path2);
+ }
+ html("</div>");
+}
+
+static void filepair_cb(struct diff_filepair *pair)
+{
+ unsigned long old_size = 0;
+ unsigned long new_size = 0;
+ int binary = 0;
+ linediff_fn print_line_fn = print_line;
+
+ if (!show_filepair(pair))
+ return;
+
+ current_filepair = pair;
+ if (use_ssdiff) {
+ cgit_ssdiff_header_begin();
+ print_line_fn = cgit_ssdiff_line_cb;
+ }
+ header(&pair->one->oid, pair->one->path, pair->one->mode,
+ &pair->two->oid, pair->two->path, pair->two->mode);
+ if (use_ssdiff)
+ cgit_ssdiff_header_end();
+ if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) {
+ if (S_ISGITLINK(pair->one->mode))
+ print_line_fn(fmt("-Subproject %s", oid_to_hex(&pair->one->oid)), 52);
+ if (S_ISGITLINK(pair->two->mode))
+ print_line_fn(fmt("+Subproject %s", oid_to_hex(&pair->two->oid)), 52);
+ if (use_ssdiff)
+ cgit_ssdiff_footer();
+ return;
+ }
+ if (cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size,
+ &new_size, &binary, ctx.qry.context,
+ ctx.qry.ignorews, print_line_fn))
+ cgit_print_error("Error running diff");
+ if (binary) {
+ if (use_ssdiff)
+ html("<tr><td colspan='4'>Binary files differ</td></tr>");
+ else
+ html("Binary files differ");
+ }
+ if (use_ssdiff)
+ cgit_ssdiff_footer();
+}
+
+void cgit_print_diff_ctrls(void)
+{
+ int i, curr;
+
+ html("<div class='cgit-panel'>");
+ html("<b>diff options</b>");
+ html("<form method='get'>");
+ cgit_add_hidden_formfields(1, 0, ctx.qry.page);
+ html("<table>");
+ html("<tr><td colspan='2'/></tr>");
+ html("<tr>");
+ html("<td class='label'>context:</td>");
+ html("<td class='ctrl'>");
+ html("<select name='context' onchange='this.form.submit();'>");
+ curr = ctx.qry.context;
+ if (!curr)
+ curr = 3;
+ for (i = 1; i <= 10; i++)
+ html_intoption(i, fmt("%d", i), curr);
+ for (i = 15; i <= 40; i += 5)
+ html_intoption(i, fmt("%d", i), curr);
+ html("</select>");
+ html("</td>");
+ html("</tr><tr>");
+ html("<td class='label'>space:</td>");
+ html("<td class='ctrl'>");
+ html("<select name='ignorews' onchange='this.form.submit();'>");
+ html_intoption(0, "include", ctx.qry.ignorews);
+ html_intoption(1, "ignore", ctx.qry.ignorews);
+ html("</select>");
+ html("</td>");
+ html("</tr><tr>");
+ html("<td class='label'>mode:</td>");
+ html("<td class='ctrl'>");
+ html("<select name='dt' onchange='this.form.submit();'>");
+ curr = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
+ html_intoption(0, "unified", curr);
+ html_intoption(1, "ssdiff", curr);
+ html_intoption(2, "stat only", curr);
+ 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>");
+}
+
+struct strbuf* cgit_gopher_add_info_tag(struct diff_options *opt, void *data){
+
+ struct strbuf* buff;
+ char *str;
+
+
+ buff = malloc(sizeof(struct strbuf));
+ str = malloc(2 * sizeof(char));
+ str[0]= 'i';
+ str[1]= '\0';
+
+ strbuf_init(buff, 2);
+ strbuf_attach(buff, str, 2, 2);
+
+ return buff;
+
+}
+
+void cgit_print_diff(const char *new_rev, const char *old_rev,
+ const char *prefix, int show_ctrls, int raw)
+{
+ struct commit *commit, *commit2;
+ const struct object_id *old_tree_oid, *new_tree_oid;
+ diff_type difftype;
+
+ /*
+ * If "follow" is set then the diff machinery needs to examine the
+ * entire commit to detect renames so we must limit the paths in our
+ * own callbacks and not pass the prefix to the diff machinery.
+ */
+ if (ctx.qry.follow && ctx.cfg.enable_follow_links) {
+ current_prefix = prefix;
+ prefix = "";
+ } else {
+ current_prefix = NULL;
+ }
+
+ if (!new_rev)
+ new_rev = ctx.qry.head;
+ if (get_oid(new_rev, new_rev_oid)) {
+ cgit_gopher_error("Bad object name");
+ return;
+ }
+ commit = lookup_commit_reference(new_rev_oid);
+ if (!commit || parse_commit(commit)) {
+ cgit_gopher_error("Bad commit");
+ return;
+ }
+ new_tree_oid = &commit->maybe_tree->object.oid;
+
+ if (old_rev) {
+ if (get_oid(old_rev, old_rev_oid)) {
+ cgit_gopher_error("Bad object name");
+ return;
+ }
+ } else if (commit->parents && commit->parents->item) {
+ oidcpy(old_rev_oid, &commit->parents->item->object.oid);
+ } else {
+ oidclr(old_rev_oid);
+ }
+
+ if (!is_null_oid(old_rev_oid)) {
+ commit2 = lookup_commit_reference(old_rev_oid);
+ if (!commit2 || parse_commit(commit2)) {
+ cgit_gopher_error("Bad commit");
+ return;
+ }
+ old_tree_oid = &commit2->maybe_tree->object.oid;
+ } else {
+ old_tree_oid = NULL;
+ }
+
+ /* FIXME: we are currently forcing raw diffs */
+ raw = 1;
+ if (raw) {
+ struct diff_options diffopt;
+
+ diff_setup(&diffopt);
+ diffopt.output_format = DIFF_FORMAT_PATCH;
+ /*diffopt.output_format = DIFF_FORMAT_RAW;*/
+ diffopt.flags.recursive = 1;
+ diffopt.output_prefix=cgit_gopher_add_info_tag;
+ diff_setup_done(&diffopt);
+
+ ctx.page.mimetype = "text/plain";
+ if (old_tree_oid) {
+ diff_tree_oid(old_tree_oid, new_tree_oid, "",
+ &diffopt);
+ } else {
+ diff_root_tree_oid(new_tree_oid, "", &diffopt);
+ }
+ diffcore_std(&diffopt);
+ diff_flush(&diffopt);
+
+ return;
+ }
+
+ difftype = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
+ use_ssdiff = difftype == DIFF_SSDIFF;
+
+ if (show_ctrls) {
+ cgit_print_layout_start();
+ /* cgit_print_diff_ctrls();*/
+ }
+
+ /*
+ * Clicking on a link to a file in the diff stat should show a diff
+ * of the file, showing the diff stat limited to a single file is
+ * pretty useless. All links from this point on will be to
+ * individual files, so we simply reset the difftype in the query
+ * here to avoid propagating DIFF_STATONLY to the individual files.
+ */
+ if (difftype == DIFF_STATONLY)
+ ctx.qry.difftype = ctx.cfg.difftype;
+
+ cgit_print_diffstat(old_rev_oid, new_rev_oid, prefix);
+
+ if (difftype == DIFF_STATONLY)
+ return;
+
+ if (use_ssdiff) {
+ html("<table summary='ssdiff' class='ssdiff'>");
+ } else {
+ html("<table summary='diff' class='diff'>");
+ html("<tr><td>");
+ }
+ cgit_diff_tree(old_rev_oid, new_rev_oid, filepair_cb, prefix,
+ ctx.qry.ignorews);
+ if (!use_ssdiff)
+ html("</td></tr>");
+
+ if (show_ctrls)
+ cgit_print_layout_end();
+}
diff --git a/ui_70-log.c b/ui_70-log.c
new file mode 100644
index 0000000..b45468e
--- /dev/null
+++ b/ui_70-log.c
@@ -0,0 +1,544 @@
+/* ui-log.c: functions for log output
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-log.h"
+#include "html.h"
+#include "ui_70-shared.h"
+#include "argv-array.h"
+
+static int files, add_lines, rem_lines, lines_counted;
+
+/*
+ * The list of available column colors in the commit graph.
+ */
+static const char *column_colors_html[] = {
+ "<span class='column1'>",
+ "<span class='column2'>",
+ "<span class='column3'>",
+ "<span class='column4'>",
+ "<span class='column5'>",
+ "<span class='column6'>",
+ "</span>",
+};
+
+#define COLUMN_COLORS_HTML_MAX (ARRAY_SIZE(column_colors_html) - 1)
+
+static void count_lines(char *line, int size)
+{
+ if (size <= 0)
+ return;
+
+ if (line[0] == '+')
+ add_lines++;
+
+ else if (line[0] == '-')
+ rem_lines++;
+}
+
+static void inspect_files(struct diff_filepair *pair)
+{
+ unsigned long old_size = 0;
+ unsigned long new_size = 0;
+ int binary = 0;
+
+ files++;
+ if (ctx.repo->enable_log_linecount)
+ cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size,
+ &new_size, &binary, 0, ctx.qry.ignorews,
+ count_lines);
+}
+
+void show_commit_decorations(struct commit *commit)
+{
+ const struct name_decoration *deco;
+ static char buf[1024];
+
+ buf[sizeof(buf) - 1] = 0;
+ deco = get_name_decoration(&commit->object);
+ if (!deco)
+ return;
+ html("<span class='decoration'>");
+ while (deco) {
+ struct object_id peeled;
+ int is_annotated = 0;
+ strncpy(buf, prettify_refname(deco->name), sizeof(buf) - 1);
+ switch(deco->type) {
+ case DECORATION_NONE:
+ /* If the git-core doesn't recognize it,
+ * don't display anything. */
+ break;
+ case DECORATION_REF_LOCAL:
+ cgit_log_link(buf, NULL, "branch-deco", buf, NULL,
+ ctx.qry.vpath, 0, NULL, NULL,
+ ctx.qry.showmsg, 0);
+ break;
+ case DECORATION_REF_TAG:
+ if (!peel_ref(deco->name, &peeled))
+ is_annotated = !oidcmp(&commit->object.oid, &peeled);
+ cgit_tag_link(buf, NULL, is_annotated ? "tag-annotated-deco" : "tag-deco", buf);
+ break;
+ case DECORATION_REF_REMOTE:
+ if (!ctx.repo->enable_remote_branches)
+ break;
+ cgit_log_link(buf, NULL, "remote-deco", NULL,
+ oid_to_hex(&commit->object.oid),
+ ctx.qry.vpath, 0, NULL, NULL,
+ ctx.qry.showmsg, 0);
+ break;
+ default:
+ cgit_commit_link(buf, NULL, "deco", ctx.qry.head,
+ oid_to_hex(&commit->object.oid),
+ ctx.qry.vpath);
+ break;
+ }
+ deco = deco->next;
+ }
+ html("</span>");
+}
+
+static void handle_rename(struct diff_filepair *pair)
+{
+ /*
+ * After we have seen a rename, we generate links to the previous
+ * name of the file so that commit & diff views get fed the path
+ * that is correct for the commit they are showing, avoiding the
+ * need to walk the entire history leading back to every commit we
+ * show in order detect renames.
+ */
+ if (0 != strcmp(ctx.qry.vpath, pair->two->path)) {
+ free(ctx.qry.vpath);
+ ctx.qry.vpath = xstrdup(pair->two->path);
+ }
+ inspect_files(pair);
+}
+
+static int show_commit(struct commit *commit, struct rev_info *revs)
+{
+ struct commit_list *parents = commit->parents;
+ struct commit *parent;
+ int found = 0, saved_fmt;
+ struct diff_flags saved_flags = revs->diffopt.flags;
+
+ /* Always show if we're not in "follow" mode with a single file. */
+ if (!ctx.qry.follow)
+ return 1;
+
+ /*
+ * In "follow" mode, we don't show merges. This is consistent with
+ * "git log --follow -- <file>".
+ */
+ if (parents && parents->next)
+ return 0;
+
+ /*
+ * If this is the root commit, do what rev_info tells us.
+ */
+ if (!parents)
+ return revs->show_root_diff;
+
+ /* When we get here we have precisely one parent. */
+ parent = parents->item;
+ /* If we can't parse the commit, let print_commit() report an error. */
+ if (parse_commit(parent))
+ return 1;
+
+ files = 0;
+ add_lines = 0;
+ rem_lines = 0;
+
+ revs->diffopt.flags.recursive = 1;
+ diff_tree_oid(&parent->maybe_tree->object.oid,
+ &commit->maybe_tree->object.oid,
+ "", &revs->diffopt);
+ diffcore_std(&revs->diffopt);
+
+ found = !diff_queue_is_empty();
+ saved_fmt = revs->diffopt.output_format;
+ revs->diffopt.output_format = DIFF_FORMAT_CALLBACK;
+ revs->diffopt.format_callback = cgit_diff_tree_cb;
+ revs->diffopt.format_callback_data = handle_rename;
+ diff_flush(&revs->diffopt);
+ revs->diffopt.output_format = saved_fmt;
+ revs->diffopt.flags = saved_flags;
+
+ lines_counted = 1;
+ return found;
+}
+
+static void print_commit(struct commit *commit, struct rev_info *revs)
+{
+ struct commitinfo *info;
+ int columns = revs->graph ? 4 : 3;
+ struct strbuf graphbuf = STRBUF_INIT;
+ struct strbuf msgbuf = STRBUF_INIT;
+
+ if (ctx.repo->enable_log_filecount)
+ columns++;
+ if (ctx.repo->enable_log_linecount)
+ columns++;
+
+ if (revs->graph) {
+ /* Advance graph until current commit */
+ while (!graph_next_line(revs->graph, &graphbuf)) {
+ /* Print graph segment in otherwise empty table row */
+ html("<tr class='nohover'><td class='commitgraph'>");
+ html(graphbuf.buf);
+ htmlf("</td><td colspan='%d' /></tr>\n", columns);
+ strbuf_setlen(&graphbuf, 0);
+ }
+ /* Current commit's graph segment is now ready in graphbuf */
+ }
+
+ info = cgit_parse_commit(commit);
+
+ cgit_gopher_start_selector(GOPHER_MENU);
+
+ if (revs->graph) {
+ /* Print graph segment for current commit */
+ cgit_gopher_text(graphbuf.buf);
+ strbuf_setlen(&graphbuf, 0);
+ }
+ else {
+ cgit_print_age(info->committer_date, info->committer_tz, TM_WEEK * 2);
+ }
+
+ if (ctx.qry.showmsg) {
+ /* line-wrap long commit subjects instead of truncating them */
+ size_t subject_len = strlen(info->subject);
+
+ if (subject_len > ctx.cfg.max_msg_len &&
+ ctx.cfg.max_msg_len >= 15) {
+ /* symbol for signaling line-wrap (in PAGE_ENCODING) */
+ const char wrap_symbol[] = { ' ', 0xE2, 0x86, 0xB5, 0 };
+ int i = ctx.cfg.max_msg_len - strlen(wrap_symbol);
+
+ /* Rewind i to preceding space character */
+ while (i > 0 && !isspace(info->subject[i]))
+ --i;
+ if (!i) /* Oops, zero spaces. Reset i */
+ i = ctx.cfg.max_msg_len - strlen(wrap_symbol);
+
+ /* add remainder starting at i to msgbuf */
+ strbuf_add(&msgbuf, info->subject + i, subject_len - i);
+ strbuf_trim(&msgbuf);
+ strbuf_add(&msgbuf, "\n\n", 2);
+
+ /* Place wrap_symbol at position i in info->subject */
+ strcpy(info->subject + i, wrap_symbol);
+ }
+ }
+ cgit_gopher_text_pad(info->subject, GOPHER_SUMMARY_DESC_LEN + 1);
+ cgit_gopher_text_pad(info->author, GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_tab();
+ cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head,
+ oid_to_hex(&commit->object.oid), ctx.qry.vpath);
+
+ cgit_gopher_end_selector();
+
+ if (revs->graph) {
+ html("</td><td>");
+ cgit_print_age(info->committer_date, info->committer_tz, TM_WEEK * 2);
+ }
+
+ if (!lines_counted && (ctx.repo->enable_log_filecount ||
+ ctx.repo->enable_log_linecount)) {
+ files = 0;
+ add_lines = 0;
+ rem_lines = 0;
+ cgit_diff_commit(commit, inspect_files, ctx.qry.vpath);
+ }
+
+/* if (ctx.repo->enable_log_filecount)
+ htmlf("</td><td>%d", files);
+ if (ctx.repo->enable_log_linecount)
+ htmlf("</td><td><span class='deletions'>-%d</span>/"
+ "<span class='insertions'>+%d</span>", rem_lines, add_lines);
+*/
+
+ if ((revs->graph && !graph_is_commit_finished(revs->graph))
+ || ctx.qry.showmsg) { /* Print a second table row */
+ html("<tr class='nohover-highlight'>");
+
+ if (ctx.qry.showmsg) {
+ /* Concatenate commit message + notes in msgbuf */
+ if (info->msg && *(info->msg)) {
+ strbuf_addstr(&msgbuf, info->msg);
+ strbuf_addch(&msgbuf, '\n');
+ }
+ format_display_notes(&commit->object.oid,
+ &msgbuf, PAGE_ENCODING, 0);
+ strbuf_addch(&msgbuf, '\n');
+ strbuf_ltrim(&msgbuf);
+ }
+
+ if (revs->graph) {
+ int lines = 0;
+
+ /* Calculate graph padding */
+ if (ctx.qry.showmsg) {
+ /* Count #lines in commit message + notes */
+ const char *p = msgbuf.buf;
+ lines = 1;
+ while ((p = strchr(p, '\n'))) {
+ p++;
+ lines++;
+ }
+ }
+
+ /* Print graph padding */
+ html("<td class='commitgraph'>");
+ while (lines > 0 || !graph_is_commit_finished(revs->graph)) {
+ if (graphbuf.len)
+ html("\n");
+ strbuf_setlen(&graphbuf, 0);
+ graph_next_line(revs->graph, &graphbuf);
+ html(graphbuf.buf);
+ lines--;
+ }
+ html("</td>\n");
+ }
+ else
+ html("<td/>"); /* Empty 'Age' column */
+
+ /* Print msgbuf into remainder of table row */
+ htmlf("<td colspan='%d'%s>\n", columns - (revs->graph ? 1 : 0),
+ ctx.qry.showmsg ? " class='logmsg'" : "");
+ html_txt(msgbuf.buf);
+ html("</td></tr>\n");
+ }
+
+ strbuf_release(&msgbuf);
+ strbuf_release(&graphbuf);
+ cgit_free_commitinfo(info);
+}
+
+static const char *disambiguate_ref(const char *ref, int *must_free_result)
+{
+ struct object_id oid;
+ struct strbuf longref = STRBUF_INIT;
+
+ strbuf_addf(&longref, "refs/heads/%s", ref);
+ if (get_oid(longref.buf, &oid) == 0) {
+ *must_free_result = 1;
+ return strbuf_detach(&longref, NULL);
+ }
+
+ *must_free_result = 0;
+ strbuf_release(&longref);
+ return ref;
+}
+
+static char *next_token(char **src)
+{
+ char *result;
+
+ if (!src || !*src)
+ return NULL;
+ while (isspace(**src))
+ (*src)++;
+ if (!**src)
+ return NULL;
+ result = *src;
+ while (**src) {
+ if (isspace(**src)) {
+ **src = '\0';
+ (*src)++;
+ break;
+ }
+ (*src)++;
+ }
+ return result;
+}
+
+void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern,
+ char *path, int pager, int commit_graph, int commit_sort)
+{
+ struct rev_info rev;
+ struct commit *commit;
+ struct argv_array rev_argv = ARGV_ARRAY_INIT;
+ int i, columns = commit_graph ? 4 : 3;
+ int must_free_tip = 0;
+
+ /* rev_argv.argv[0] will be ignored by setup_revisions */
+ argv_array_push(&rev_argv, "log_rev_setup");
+
+ if (!tip)
+ tip = ctx.qry.head;
+ tip = disambiguate_ref(tip, &must_free_tip);
+ argv_array_push(&rev_argv, tip);
+
+ if (grep && pattern && *pattern) {
+ pattern = xstrdup(pattern);
+ if (!strcmp(grep, "grep") || !strcmp(grep, "author") ||
+ !strcmp(grep, "committer")) {
+ argv_array_pushf(&rev_argv, "--%s=%s", grep, pattern);
+ } else if (!strcmp(grep, "range")) {
+ char *arg;
+ /* Split the pattern at whitespace and add each token
+ * as a revision expression. Do not accept other
+ * rev-list options. Also, replace the previously
+ * pushed tip (it's no longer relevant).
+ */
+ argv_array_pop(&rev_argv);
+ while ((arg = next_token(&pattern))) {
+ if (*arg == '-') {
+ fprintf(stderr, "Bad range expr: %s\n",
+ arg);
+ break;
+ }
+ argv_array_push(&rev_argv, arg);
+ }
+ }
+ }
+
+ if (!path || !ctx.cfg.enable_follow_links) {
+ /*
+ * If we don't have a path, "follow" is a no-op so make sure
+ * the variable is set to false to avoid needing to check
+ * both this and whether we have a path everywhere.
+ */
+ ctx.qry.follow = 0;
+ }
+
+ if (commit_graph && !ctx.qry.follow) {
+ argv_array_push(&rev_argv, "--graph");
+ argv_array_push(&rev_argv, "--color");
+ graph_set_column_colors(column_colors_html,
+ COLUMN_COLORS_HTML_MAX);
+ }
+
+ if (commit_sort == 1)
+ argv_array_push(&rev_argv, "--date-order");
+ else if (commit_sort == 2)
+ argv_array_push(&rev_argv, "--topo-order");
+
+ if (path && ctx.qry.follow)
+ argv_array_push(&rev_argv, "--follow");
+ argv_array_push(&rev_argv, "--");
+ if (path)
+ argv_array_push(&rev_argv, path);
+
+ init_revisions(&rev, NULL);
+ rev.abbrev = DEFAULT_ABBREV;
+ rev.commit_format = CMIT_FMT_DEFAULT;
+ rev.verbose_header = 1;
+ rev.show_root_diff = 0;
+ rev.ignore_missing = 1;
+ rev.simplify_history = 1;
+ setup_revisions(rev_argv.argc, rev_argv.argv, &rev, NULL);
+ load_ref_decorations(NULL, DECORATE_FULL_REFS);
+ rev.show_decorations = 1;
+ rev.grep_filter.ignore_case = 1;
+
+ rev.diffopt.detect_rename = 1;
+ rev.diffopt.rename_limit = ctx.cfg.renamelimit;
+ if (ctx.qry.ignorews)
+ DIFF_XDL_SET(&rev.diffopt, IGNORE_WHITESPACE);
+
+ compile_grep_patterns(&rev.grep_filter);
+ prepare_revision_walk(&rev);
+
+ if (pager) {
+ cgit_print_layout_start();
+ /*html("<table class='list nowrap'>");*/
+ }
+
+ cgit_gopher_start_selector(GOPHER_INFO);
+ if (commit_graph)
+ cgit_gopher_text_pad("", GOPHER_SUMMARY_DATE_LEN);
+ else
+ cgit_gopher_text_pad("Age", GOPHER_SUMMARY_DATE_LEN);
+ cgit_gopher_text_pad("Commit message", GOPHER_SUMMARY_DESC_LEN);
+ /*if (pager) {
+ html(" (");
+ cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL,
+ NULL, ctx.qry.head, ctx.qry.sha1,
+ ctx.qry.vpath, ctx.qry.ofs, ctx.qry.grep,
+ ctx.qry.search, ctx.qry.showmsg ? 0 : 1,
+ ctx.qry.follow);
+ html(")");
+ }*/
+ cgit_gopher_text_pad("Author", GOPHER_SUMMARY_NAME_LEN);
+ if (rev.graph)
+ cgit_gopher_text_pad("Age", GOPHER_SUMMARY_AGE_LEN);
+ if (ctx.repo->enable_log_filecount) {
+ cgit_gopher_text_pad("Files", GOPHER_SUMMARY_MODE_LEN);
+ columns++;
+ }
+ if (ctx.repo->enable_log_linecount) {
+ cgit_gopher_text_pad("Lines", GOPHER_SUMMARY_MODE_LEN);
+ columns++;
+ }
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+
+ if (ofs<0)
+ ofs = 0;
+
+ for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; /* nop */) {
+ if (show_commit(commit, &rev))
+ i++;
+ free_commit_buffer(commit);
+ free_commit_list(commit->parents);
+ commit->parents = NULL;
+ }
+
+ for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; /* nop */) {
+ /*
+ * In "follow" mode, we must count the files and lines the
+ * first time we invoke diff on a given commit, and we need
+ * to do that to see if the commit touches the path we care
+ * about, so we do it in show_commit. Hence we must clear
+ * lines_counted here.
+ *
+ * This has the side effect of avoiding running diff twice
+ * when we are both following renames and showing file
+ * and/or line counts.
+ */
+ lines_counted = 0;
+ if (show_commit(commit, &rev)) {
+ i++;
+ print_commit(commit, &rev);
+ }
+ free_commit_buffer(commit);
+ free_commit_list(commit->parents);
+ commit->parents = NULL;
+ }
+ if (pager) {
+ if (ofs > 0) {
+ html("<li>");
+ cgit_log_link("[prev]", NULL, NULL, ctx.qry.head,
+ ctx.qry.sha1, ctx.qry.vpath,
+ ofs - cnt, ctx.qry.grep,
+ ctx.qry.search, ctx.qry.showmsg,
+ ctx.qry.follow);
+ html("</li>");
+ }
+ if ((commit = get_revision(&rev)) != NULL) {
+ html("<li>");
+ cgit_log_link("[next]", NULL, NULL, ctx.qry.head,
+ ctx.qry.sha1, ctx.qry.vpath,
+ ofs + cnt, ctx.qry.grep,
+ ctx.qry.search, ctx.qry.showmsg,
+ ctx.qry.follow);
+ html("</li>");
+ }
+ cgit_print_layout_end();
+ } else if ((commit = get_revision(&rev)) != NULL) {
+ cgit_gopher_log_link("[...]", NULL, NULL, ctx.qry.head, NULL,
+ ctx.qry.vpath, 0, NULL, NULL, ctx.qry.showmsg,
+ ctx.qry.follow);
+ }
+
+ /* If we allocated tip then it is safe to cast away const. */
+ if (must_free_tip)
+ free((char*) tip);
+}
diff --git a/ui_70-patch.c b/ui_70-patch.c
new file mode 100644
index 0000000..70f159e
--- /dev/null
+++ b/ui_70-patch.c
@@ -0,0 +1,92 @@
+/* ui-patch.c: generate patch view
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-patch.h"
+#include "html.h"
+#include "ui_70-shared.h"
+
+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[2 * 40 + 3];
+ 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 (get_oid(new_rev, &new_rev_oid)) {
+ cgit_gopher_error("Bad object id");
+ return;
+ }
+ commit = lookup_commit_reference(&new_rev_oid);
+ if (!commit) {
+ cgit_gopher_error("Bad commit reference");
+ return;
+ }
+
+ if (old_rev) {
+ if (get_oid(old_rev, &old_rev_oid)) {
+ cgit_gopher_error("Bad object id");
+ return;
+ }
+ if (!lookup_commit_reference(&old_rev_oid)) {
+ cgit_gopher_error("Bad commit reference");
+ return;
+ }
+ } else if (commit->parents && commit->parents->item) {
+ oidcpy(&old_rev_oid, &commit->parents->item->object.oid);
+ } else {
+ oidclr(&old_rev_oid);
+ }
+
+ if (is_null_oid(&old_rev_oid)) {
+ memcpy(rev_range, oid_to_hex(&new_rev_oid), GIT_SHA1_HEXSZ + 1);
+ } else {
+ sprintf(rev_range, "%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";
+ }
+
+ init_revisions(&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/ui_70-refs.c b/ui_70-refs.c
new file mode 100644
index 0000000..457e69f
--- /dev/null
+++ b/ui_70-refs.c
@@ -0,0 +1,218 @@
+/* ui-refs.c: browse symbolic refs
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-refs.h"
+#include "html.h"
+#include "ui_70-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;
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text_pad(name, GOPHER_SUMMARY_NAME_LEN);
+ if (ref->object->type == OBJ_COMMIT) {
+ cgit_gopher_text_pad(info->subject, GOPHER_SUMMARY_DESC_LEN);
+ cgit_gopher_text_pad(info->author, GOPHER_SUMMARY_AUTH_LEN);
+ cgit_print_age(info->committer_date, info->committer_tz, -1);
+ } else {
+ cgit_object_link(ref->object);
+ }
+ cgit_gopher_tab();
+ cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL,
+ ctx.qry.showmsg, 0);
+ cgit_gopher_end_selector();
+ return 0;
+}
+
+
+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;
+ }
+
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text_pad(name, GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text_pad(oid_to_hex(&obj->oid), GOPHER_SUMMARY_DESC_LEN);
+ if (info) {
+ if (info->tagger) {
+ cgit_gopher_text_pad(info->tagger, GOPHER_SUMMARY_AUTH_LEN);
+ }
+ } else if (ref->object->type == OBJ_COMMIT) {
+ cgit_gopher_text_pad(ref->commit->author, GOPHER_SUMMARY_AUTH_LEN);
+ }
+ 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);
+ }
+ cgit_gopher_text("\t");
+ cgit_tag_link(name, NULL, NULL, name);
+ cgit_gopher_end_selector();
+ return 0;
+}
+
+static void print_refs_link(char *path)
+{
+ cgit_gopher_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path);
+}
+
+void print_branch_header(void){
+
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text_pad("Branch", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text_pad("Commit message", GOPHER_SUMMARY_DESC_LEN);
+ cgit_gopher_text_pad("Author", GOPHER_SUMMARY_AUTH_LEN);
+ cgit_gopher_text_pad("Age", GOPHER_SUMMARY_AGE_LEN);
+ cgit_gopher_text("\t");
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+
+}
+
+
+void cgit_print_branches(int maxcount)
+{
+ struct reflist list;
+ int i;
+
+ print_branch_header();
+ list.refs = NULL;
+ list.alloc = list.count = 0;
+ for_each_branch_ref(cgit_refs_cb, &list);
+ if (ctx.repo->enable_remote_branches)
+ for_each_remote_ref(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);
+}
+
+static void print_tag_header(void){
+
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text_pad("Tag", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text_pad("Commit", GOPHER_SUMMARY_DESC_LEN);
+ cgit_gopher_text_pad("Author", GOPHER_SUMMARY_AUTH_LEN);
+ cgit_gopher_text_pad("Age", GOPHER_SUMMARY_AGE_LEN);
+ cgit_gopher_text("\t");
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+
+}
+
+
+void cgit_print_tags(int maxcount)
+{
+ struct reflist list;
+ int i;
+
+ list.refs = NULL;
+ list.alloc = list.count = 0;
+ for_each_tag_ref(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();
+
+ 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);
+ cgit_print_tags(0);
+ }
+ cgit_print_layout_end();
+}
diff --git a/ui_70-repolist.c b/ui_70-repolist.c
new file mode 100644
index 0000000..40d9619
--- /dev/null
+++ b/ui_70-repolist.c
@@ -0,0 +1,331 @@
+/* ui-repolist.c: functions for generating the repolist page
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * 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_70-shared.h"
+
+static time_t read_agefile(char *path)
+{
+ time_t result;
+ size_t size;
+ char *buf = NULL;
+ struct strbuf date_buf = STRBUF_INIT;
+
+ if (readfile(path, &buf, &size)) {
+ free(buf);
+ return -1;
+ }
+
+ 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);
+ else
+ cgit_gopher_text_pad("----", GOPHER_SUMMARY_DATE_LEN);
+}
+
+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){
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- ctx.qry.url is NULL\n");
+#endif
+ return 1;
+ }
+ if (repo->url && starts_with(repo->url, ctx.qry.url)){
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- repo URL not in qry.url\n");
+#endif
+ return 1;
+ }
+ return 0;
+}
+
+static int is_visible(struct cgit_repo *repo)
+{
+ if (repo->hide || repo->ignore){
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- repo: '%s' is invisible or ignored", repo->name);
+#endif
+ return 0;
+ }
+ if (!(is_match(repo) && is_in_url(repo))){
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- !(is_match(repo) && is_in_url(repo))\n");
+#endif
+ return 0;
+ }
+ return 1;
+}
+
+static int any_repos_visible(void)
+{
+ int i;
+
+#ifdef DEBUG_GOPHER
+ fprintf(stdout, "i -- ctx.qry.search: %s\n", ctx.qry.search);
+#endif
+ for (i = 0; i < cgit_repolist.count; i++) {
+ if (is_visible(&cgit_repolist.repos[i]))
+ return 1;
+ }
+ return 0;
+}
+
+
+static void print_header(void)
+{
+ cgit_gopher_info("Repositories");
+ cgit_gopher_info("____________");
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text_pad("Name", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text_pad("Decription", GOPHER_SUMMARY_DESC_LEN);
+ cgit_gopher_text_pad("Modified", GOPHER_SUMMARY_DATE_LEN);
+ cgit_gopher_text("\t");
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+}
+
+
+
+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;
+ char *last_section = NULL;
+ char *section;
+ int sorted = 0;
+
+ if (!any_repos_visible()) {
+ cgit_gopher_error("No visible repositories found");
+ return;
+ }
+
+ if (ctx.cfg.enable_index_links)
+ ++columns;
+ if (ctx.cfg.enable_index_owner)
+ ++columns;
+
+ ctx.page.title = ctx.cfg.root_title;
+
+ if (ctx.qry.sort)
+ sorted = sort_repolist(ctx.qry.sort);
+ else if (ctx.cfg.section_sort)
+ sort_repolist("section");
+
+ cgit_print_layout_start();
+ print_header();
+
+ 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;
+ 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)))) {
+ cgit_gopher_info(section);
+ last_section = section;
+ }
+
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text_pad(ctx.repo->name, GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text_pad(ctx.repo->desc, GOPHER_SUMMARY_DESC_LEN);
+ print_modtime(ctx.repo);
+ cgit_gopher_text("\t");
+ cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
+ cgit_gopher_end_selector();
+ }
+}
+
+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/ui_70-shared.c b/ui_70-shared.c
new file mode 100644
index 0000000..9abf94c
--- /dev/null
+++ b/ui_70-shared.c
@@ -0,0 +1,1345 @@
+/* ui-shared.c: common web output functions
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include <stdio.h>
+#include "cgit.h"
+#include "html.h"
+#include "ui_70-shared.h"
+
+
+void cgit_gopher_selector(char type, char *str, char *sel, const char *host, const char *port){
+
+ printf("%c%s\t%s\t%s\t%s\r\n",
+ type, str, sel, host, port);
+}
+
+
+void cgit_gopher_info(char *msg){
+ cgit_gopher_selector('i', msg, "Err", ctx.env.server_name, ctx.env.server_port);
+}
+
+void cgit_gopher_menu(char *descr, char *sel){
+
+ cgit_gopher_selector('1', descr, sel, ctx.env.server_name, ctx.env.server_port);
+}
+
+void cgit_gopher_textfile(char *descr, char *sel){
+
+ cgit_gopher_selector('0', descr, sel, ctx.env.server_name, ctx.env.server_port);
+}
+
+void cgit_gopher_error(char *msg){
+ cgit_gopher_selector('3', msg, "Err", ctx.env.server_name, ctx.env.server_port);
+}
+
+
+void cgit_gopher_start_selector(char type){
+
+ printf("%c", type);
+}
+
+void cgit_gopher_selector_descr(const char *descr){
+
+ printf("%s\t", descr);
+}
+
+void cgit_gopher_selector_link(const char *sel){
+
+ printf("%s\t", sel);
+}
+
+void cgit_gopher_text(const char *txt){
+
+ printf("%s", txt);
+}
+
+void cgit_gopher_tab(){
+ printf("\t");
+}
+
+
+void gopher_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);
+ printf(buf.buf);
+ strbuf_release(&buf);
+}
+
+
+void gopherf(const char *format, ...)
+{
+ va_list args;
+ struct strbuf buf = STRBUF_INIT;
+
+ va_start(args, format);
+ strbuf_vaddf(&buf, format, args);
+ va_end(args);
+ cgit_gopher_text(buf.buf);
+ strbuf_release(&buf);
+}
+
+
+
+void gopher_fileperm(unsigned short mode)
+{
+ gopherf("%c%c%c", (mode & 4 ? 'r' : '-'),
+ (mode & 2 ? 'w' : '-'), (mode & 1 ? 'x' : '-'));
+}
+
+
+void cgit_gopher_textf(const char *fmt, va_list ap){
+
+ va_list cp;
+ va_copy(cp, ap);
+ gopher_vtxtf(fmt, cp);
+ va_end(cp);
+}
+
+
+void cgit_gopher_text_pad(const char *txt, int len){
+
+ static char fmt[80];
+ int sz;
+
+ memset(fmt, 0, 80 * sizeof(char));
+ sz = snprintf(fmt, len, "%s", txt);
+ while(sz < len ){
+ fmt[sz++] = GOPHER_PAD_CHAR;
+ }
+ fmt[sz] = '\0';
+ printf(fmt);
+}
+
+
+void cgit_gopher_end_selector(){
+
+ printf("%s\t%s\r\n", ctx.env.server_name, ctx.env.server_port);
+}
+
+
+void cgit_tree_link_gopher(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+
+
+
+}
+
+
+
+
+/* print an error message with format */
+void cgit_vprint_error(const char *fmt, va_list ap)
+{
+ va_list cp;
+ va_copy(cp, ap);
+ gopher_vtxtf(fmt, cp);
+ va_end(cp);
+}
+
+
+void cgit_print_error_page(int code, const char *msg, const char *fmt, ...)
+{
+ va_list ap;
+ ctx.page.expires = ctx.cfg.cache_dynamic_ttl;
+ ctx.page.status = code;
+ ctx.page.statusmsg = msg;
+ va_start(ap, fmt);
+ cgit_vprint_error(fmt, ap);
+ va_end(ap);
+}
+
+
+/* ui-shared.c: common web output functions
+ *
+ * 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 "ui-shared.h"
+#include "cmd.h"
+#include "html.h"
+#include "version.h"
+
+static const char cgit_doctype[] =
+"<!DOCTYPE html>\n";
+
+static char *http_date(time_t t)
+{
+ static char day[][4] =
+ {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
+ static char month[][4] =
+ {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
+ struct tm *tm = gmtime(&t);
+ return fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm->tm_wday],
+ tm->tm_mday, month[tm->tm_mon], 1900 + tm->tm_year,
+ tm->tm_hour, tm->tm_min, tm->tm_sec);
+}
+
+void cgit_print_error(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ cgit_vprint_error(fmt, ap);
+ va_end(ap);
+}
+
+/*
+void cgit_vprint_error(const char *fmt, va_list ap)
+{
+ va_list cp;
+ html("<div class='error'>");
+ va_copy(cp, ap);
+ html_vtxtf(fmt, cp);
+ va_end(cp);
+ html("</div>\n");
+}
+*/
+
+const char *cgit_httpscheme(void)
+{
+ if (ctx.env.https && !strcmp(ctx.env.https, "on"))
+ return "https://";
+ else
+ return "http://";
+}
+
+char *cgit_hosturl(void)
+{
+ if (ctx.env.http_host)
+ return xstrdup(ctx.env.http_host);
+ if (!ctx.env.server_name)
+ return NULL;
+ if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80)
+ return xstrdup(ctx.env.server_name);
+ return fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port);
+}
+
+char *cgit_currenturl(void)
+{
+ const char *root = cgit_rooturl();
+ size_t len = strlen(root);
+
+ if (!ctx.qry.url)
+ return fmtalloc("/%s", root);
+ if (len && root[len - 1] == '/')
+ return fmtalloc("/%s%s", root, ctx.qry.url);
+ return fmtalloc("/%s/%s", root, ctx.qry.url);
+}
+
+const char *cgit_rooturl(void)
+{
+/* if (ctx.cfg.virtual_root)
+ return ctx.cfg.virtual_root;
+ else*/
+ return fmtalloc("/%s", ctx.cfg.script_name);
+}
+
+const char *cgit_loginurl(void)
+{
+ static const char *login_url;
+ if (!login_url)
+ login_url = fmtalloc("%s?p=login", cgit_rooturl());
+ return login_url;
+}
+
+char *cgit_repourl(const char *reponame)
+{
+ if (ctx.cfg.virtual_root)
+ return fmtalloc("%s%s/", ctx.cfg.virtual_root, reponame);
+ else
+ return fmtalloc("?r=%s", reponame);
+}
+
+char *cgit_fileurl(const char *reponame, const char *pagename,
+ const char *filename, const char *query)
+{
+ struct strbuf sb = STRBUF_INIT;
+ char *delim;
+
+ if (ctx.cfg.virtual_root) {
+ strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame,
+ pagename, (filename ? filename:""));
+ delim = "?";
+ } else {
+ strbuf_addf(&sb, "?url=%s/%s/%s", reponame, pagename,
+ (filename ? filename : ""));
+ delim = "&amp;";
+ }
+ if (query)
+ strbuf_addf(&sb, "%s%s", delim, query);
+ return strbuf_detach(&sb, NULL);
+}
+
+char *cgit_pageurl(const char *reponame, const char *pagename,
+ const char *query)
+{
+ return cgit_fileurl(reponame, pagename, NULL, query);
+}
+
+const char *cgit_repobasename(const char *reponame)
+{
+ /* I assume we don't need to store more than one repo basename */
+ static char rvbuf[1024];
+ int p;
+ const char *rv;
+ size_t len;
+
+ len = strlcpy(rvbuf, reponame, sizeof(rvbuf));
+ if (len >= sizeof(rvbuf))
+ die("cgit_repobasename: truncated repository name '%s'", reponame);
+ p = len - 1;
+ /* strip trailing slashes */
+ while (p && rvbuf[p] == '/')
+ rvbuf[p--] = '\0';
+ /* strip trailing .git */
+ if (p >= 3 && starts_with(&rvbuf[p-3], ".git")) {
+ p -= 3;
+ rvbuf[p--] = '\0';
+ }
+ /* strip more trailing slashes if any */
+ while (p && rvbuf[p] == '/')
+ rvbuf[p--] = '\0';
+ /* find last slash in the remaining string */
+ rv = strrchr(rvbuf, '/');
+ if (rv)
+ return ++rv;
+ return rvbuf;
+}
+
+const char *cgit_snapshot_prefix(const struct cgit_repo *repo)
+{
+ if (repo->snapshot_prefix)
+ return repo->snapshot_prefix;
+
+ return cgit_repobasename(repo->url);
+}
+
+static void site_url(const char *page, const char *search, const char *sort, int ofs, int always_root)
+{
+ char *delim = "?";
+
+ if (always_root || page){
+
+ cgit_gopher_text(cgit_rooturl());
+ }
+ else {
+ char *currenturl = cgit_currenturl();
+ cgit_gopher_text(currenturl);
+ free(currenturl);
+ }
+
+ if (page) {
+ gopherf("?p=%s", page);
+ delim = "&";
+ }
+ if (search) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("q=");
+ cgit_gopher_text(search);
+ delim = "&";
+ }
+ if (sort) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("s=");
+ cgit_gopher_text(sort);
+ delim = "&";
+ }
+ if (ofs) {
+ cgit_gopher_text(delim);
+ gopherf("ofs=%d", ofs);
+ }
+}
+
+static void site_link(const char *page, const char *name, const char *title,
+ const char *class, const char *search, const char *sort, int ofs, int always_root)
+{
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text(name);
+ cgit_gopher_tab();
+ site_url(page, search, sort, ofs, always_root);
+ cgit_gopher_tab();
+ cgit_gopher_end_selector();
+}
+
+void cgit_index_link(const char *name, const char *title, const char *class,
+ const char *pattern, const char *sort, int ofs, int always_root)
+{
+ site_link(NULL, name, title, class, pattern, sort, ofs, always_root);
+}
+
+void cgit_gopher_index_link(const char *name, const char *title, const char *class,
+ const char *pattern, const char *sort, int ofs, int always_root)
+{
+
+ cgit_index_link(name, title, class, pattern, sort, ofs, always_root);
+}
+
+/*
+static char *repolink(const char *title, const char *class, const char *page,
+ const char *head, const char *path)
+{
+ char *delim = "?";
+
+ html("<a");
+ if (title) {
+ html(" title='");
+ html_attr(title);
+ html("'");
+ }
+ if (class) {
+ html(" class='");
+ html_attr(class);
+ html("'");
+ }
+ html(" href='");
+ if (ctx.cfg.virtual_root) {
+ html_url_path(ctx.cfg.virtual_root);
+ html_url_path(ctx.repo->url);
+ if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
+ html("/");
+ if (page) {
+ html_url_path(page);
+ html("/");
+ if (path)
+ html_url_path(path);
+ }
+ } else {
+ html_url_path(ctx.cfg.script_name);
+ html("?url=");
+ html_url_arg(ctx.repo->url);
+ if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
+ html("/");
+ if (page) {
+ html_url_arg(page);
+ html("/");
+ if (path)
+ html_url_arg(path);
+ }
+ delim = "&amp;";
+ }
+ if (head && ctx.repo->defbranch && strcmp(head, ctx.repo->defbranch)) {
+ html(delim);
+ html("h=");
+ html_url_arg(head);
+ delim = "&amp;";
+ }
+ return fmt("%s", delim);
+}
+
+*/
+
+
+static char *repolink(const char *title, const char *class, const char *page,
+ const char *head, const char *path)
+{
+ char *delim = "?";
+
+ cgit_gopher_text("/");
+ cgit_gopher_text(ctx.cfg.script_name);
+ cgit_gopher_text("?url=");
+ cgit_gopher_text(ctx.repo->url);
+ if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
+ cgit_gopher_text("/");
+ if (page) {
+ cgit_gopher_text(page);
+ cgit_gopher_text("/");
+ if (path)
+ cgit_gopher_text(path);
+ }
+ delim = "&";
+ if (head && ctx.repo->defbranch && strcmp(head, ctx.repo->defbranch)) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("h=");
+ cgit_gopher_text(head);
+ delim = "&";
+ }
+ return fmt("%s", delim);
+}
+
+static void reporevlink(const char *page, const char *name, const char *title,
+ const char *class, const char *head, const char *rev,
+ const char *path)
+{
+ char *delim;
+
+
+ delim = repolink(title, class, page, head, path);
+ if (rev && ctx.qry.head != NULL && strcmp(rev, ctx.qry.head)) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("id=");
+ cgit_gopher_text(rev);
+ }
+ cgit_gopher_tab();
+}
+
+void cgit_summary_link(const char *name, const char *title, const char *class,
+ const char *head)
+{
+
+ reporevlink(NULL, name, title, class, head, NULL, NULL);
+}
+
+
+void cgit_gopher_summary_link(const char *name, const char *title, const char *class,
+ const char *head)
+{
+
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text(name);
+ cgit_gopher_tab();
+ cgit_summary_link(name, title, class, head);
+ cgit_gopher_end_selector();
+}
+
+void cgit_tag_link(const char *name, const char *title, const char *class,
+ const char *tag)
+{
+ reporevlink("tag", name, title, class, tag, NULL, NULL);
+}
+
+void cgit_tree_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+
+ reporevlink("tree", name, title, class, head, rev, path);
+}
+
+
+void cgit_gopher_tree_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text(name);
+ cgit_gopher_tab();
+ cgit_tree_link(name, title, class, head, rev, path);
+ cgit_gopher_end_selector();
+}
+
+void cgit_plain_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+ reporevlink("plain", name, title, class, head, rev, path);
+}
+
+void cgit_blame_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+ reporevlink("blame", name, title, class, head, rev, path);
+}
+
+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)
+{
+ char *delim;
+
+ delim = repolink(title, class, "log", head, path);
+ if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("id=");
+ html_url_arg(rev);
+ delim = "&";
+ }
+ if (grep && pattern) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("qt=");
+ html_url_arg(grep);
+ delim = "&";
+ cgit_gopher_text(delim);
+ cgit_gopher_text("q=");
+ html_url_arg(pattern);
+ }
+ if (ofs > 0) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("ofs=");
+ gopherf("%d", ofs);
+ delim = "&";
+ }
+ if (showmsg) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("showmsg=1");
+ delim = "&";
+ }
+ if (follow) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("follow=1");
+ }
+ cgit_gopher_tab();
+}
+
+
+void cgit_gopher_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)
+{
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text(name);
+ cgit_gopher_tab();
+ cgit_log_link(name, title, class, head, rev, path, ofs, grep, pattern, showmsg, follow);
+ cgit_gopher_end_selector();
+
+}
+
+void cgit_commit_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+ char *delim;
+
+ delim = repolink(title, class, "commit", head, path);
+ if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("id=");
+ cgit_gopher_text(rev);
+ delim = "&";
+ }
+ if (ctx.qry.difftype) {
+ cgit_gopher_text(delim);
+ gopherf("dt=%d", ctx.qry.difftype);
+ delim = "&";
+ }
+ if (ctx.qry.context > 0 && ctx.qry.context != 3) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("context=");
+ gopherf("%d", ctx.qry.context);
+ delim = "&";
+ }
+ if (ctx.qry.ignorews) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("ignorews=1");
+ delim = "&";
+ }
+ if (ctx.qry.follow) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("follow=1");
+ }
+/* if (name[0] != '\0') {
+ cgit_gopher_text(name);
+ } else
+ cgit_gopher_text("(no commit message)");
+*/ cgit_gopher_tab();
+}
+
+
+void cgit_gopher_commit_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text(name);
+ cgit_gopher_tab();
+ cgit_commit_link(name, title, class, head, rev, path);
+ cgit_gopher_end_selector();
+}
+
+
+void cgit_refs_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+ reporevlink("refs", name, title, class, head, rev, path);
+}
+
+
+void cgit_gopher_refs_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text(name);
+ cgit_gopher_tab();
+ cgit_refs_link(name, title, class, head, rev, path);
+ cgit_gopher_end_selector();
+}
+
+void cgit_snapshot_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev,
+ const char *archivename)
+{
+ reporevlink("snapshot", name, title, class, head, rev, archivename);
+}
+
+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)
+{
+ char *delim;
+
+ delim = repolink(title, class, "diff", head, path);
+ if (new_rev && ctx.qry.head != NULL && strcmp(new_rev, ctx.qry.head)) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("id=");
+ cgit_gopher_text(new_rev);
+ delim = "&";
+ }
+ if (old_rev) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("id2=");
+ cgit_gopher_text(old_rev);
+ delim = "&";
+ }
+ if (ctx.qry.difftype) {
+ cgit_gopher_text(delim);
+ gopherf("dt=%d", ctx.qry.difftype);
+ delim = "&";
+ }
+ if (ctx.qry.context > 0 && ctx.qry.context != 3) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("context=");
+ gopherf("%d", ctx.qry.context);
+ delim = "&";
+ }
+ if (ctx.qry.ignorews) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("ignorews=1");
+ delim = "&";
+ }
+ if (ctx.qry.follow) {
+ cgit_gopher_text(delim);
+ cgit_gopher_text("follow=1");
+ }
+ cgit_gopher_tab();
+}
+
+
+void cgit_gopher_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)
+{
+
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text(name);
+ cgit_gopher_tab();
+ cgit_diff_link(name, title, class, head, new_rev, old_rev, path);
+ cgit_gopher_end_selector();
+
+}
+
+void cgit_patch_link(const char *name, const char *title, const char *class,
+ const char *head, const char *rev, const char *path)
+{
+ reporevlink("patch", name, title, class, head, rev, path);
+}
+
+void cgit_stats_link(const char *name, const char *title, const char *class,
+ const char *head, const char *path)
+{
+ reporevlink("stats", name, title, class, head, NULL, path);
+}
+
+static void cgit_self_link(char *name, const char *title, const char *class)
+{
+ if (!strcmp(ctx.qry.page, "repolist"))
+ cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort,
+ ctx.qry.ofs, 1);
+ else if (!strcmp(ctx.qry.page, "summary"))
+ cgit_summary_link(name, title, class, ctx.qry.head);
+ else if (!strcmp(ctx.qry.page, "tag"))
+ cgit_tag_link(name, title, class, ctx.qry.has_sha1 ?
+ ctx.qry.sha1 : ctx.qry.head);
+ else if (!strcmp(ctx.qry.page, "tree"))
+ cgit_tree_link(name, title, class, ctx.qry.head,
+ ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
+ ctx.qry.path);
+ else if (!strcmp(ctx.qry.page, "plain"))
+ cgit_plain_link(name, title, class, ctx.qry.head,
+ ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
+ ctx.qry.path);
+ else if (!strcmp(ctx.qry.page, "blame"))
+ cgit_blame_link(name, title, class, ctx.qry.head,
+ ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
+ ctx.qry.path);
+ else if (!strcmp(ctx.qry.page, "log"))
+ cgit_log_link(name, title, class, ctx.qry.head,
+ ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
+ ctx.qry.path, ctx.qry.ofs,
+ ctx.qry.grep, ctx.qry.search,
+ ctx.qry.showmsg, ctx.qry.follow);
+ else if (!strcmp(ctx.qry.page, "commit"))
+ cgit_commit_link(name, title, class, ctx.qry.head,
+ ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
+ ctx.qry.path);
+ else if (!strcmp(ctx.qry.page, "patch"))
+ cgit_patch_link(name, title, class, ctx.qry.head,
+ ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
+ ctx.qry.path);
+ else if (!strcmp(ctx.qry.page, "refs"))
+ cgit_refs_link(name, title, class, ctx.qry.head,
+ ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
+ ctx.qry.path);
+ else if (!strcmp(ctx.qry.page, "snapshot"))
+ cgit_snapshot_link(name, title, class, ctx.qry.head,
+ ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
+ ctx.qry.path);
+ else if (!strcmp(ctx.qry.page, "diff"))
+ cgit_diff_link(name, title, class, ctx.qry.head,
+ ctx.qry.sha1, ctx.qry.sha2,
+ ctx.qry.path);
+ else if (!strcmp(ctx.qry.page, "stats"))
+ cgit_stats_link(name, title, class, ctx.qry.head,
+ ctx.qry.path);
+ else {
+ /* Don't known how to make link for this page */
+ repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path);
+ html("><!-- cgit_self_link() doesn't know how to make link for page '");
+ html_txt(ctx.qry.page);
+ html("' -->");
+ html_txt(name);
+ html("</a>");
+ }
+}
+
+void cgit_object_link(struct object *obj)
+{
+ char *page, *shortrev, *fullrev, *name;
+
+ fullrev = oid_to_hex(&obj->oid);
+ shortrev = xstrdup(fullrev);
+ shortrev[10] = '\0';
+ if (obj->type == OBJ_COMMIT) {
+ cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
+ ctx.qry.head, fullrev, NULL);
+ return;
+ } else if (obj->type == OBJ_TREE)
+ page = "tree";
+ else if (obj->type == OBJ_TAG)
+ page = "tag";
+ else
+ page = "blob";
+ name = fmt("%s %s...", type_name(obj->type), shortrev);
+ reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
+}
+
+static struct string_list_item *lookup_path(struct string_list *list,
+ const char *path)
+{
+ struct string_list_item *item;
+
+ while (path && path[0]) {
+ if ((item = string_list_lookup(list, path)))
+ return item;
+ if (!(path = strchr(path, '/')))
+ break;
+ path++;
+ }
+ return NULL;
+}
+
+void cgit_submodule_link(const char *class, char *path, const char *rev)
+{
+ struct string_list *list;
+ struct string_list_item *item;
+ char tail, *dir;
+ size_t len;
+
+ len = 0;
+ tail = 0;
+ list = &ctx.repo->submodules;
+ item = lookup_path(list, path);
+ if (!item) {
+ len = strlen(path);
+ tail = path[len - 1];
+ if (tail == '/') {
+ path[len - 1] = 0;
+ item = lookup_path(list, path);
+ }
+ }
+ if (item || ctx.repo->module_link) {
+ html("<a ");
+ if (class)
+ htmlf("class='%s' ", class);
+ html("href='");
+ if (item) {
+ html_attrf(item->util, rev);
+ } else {
+ dir = strrchr(path, '/');
+ if (dir)
+ dir++;
+ else
+ dir = path;
+ html_attrf(ctx.repo->module_link, dir, rev);
+ }
+ html("'>");
+ html_txt(path);
+ html("</a>");
+ } else {
+ html("<span");
+ if (class)
+ htmlf(" class='%s'", class);
+ html(">");
+ html_txt(path);
+ html("</span>");
+ }
+ html_txtf(" @ %.7s", rev);
+ if (item && tail)
+ path[len - 1] = tail;
+}
+
+const struct date_mode *cgit_date_mode(enum date_mode_type type)
+{
+ static struct date_mode mode;
+ mode.type = type;
+ mode.local = ctx.cfg.local_time;
+ return &mode;
+}
+
+
+void cgit_print_age(time_t t, int tz, time_t max_relative)
+{
+
+ if (!t)
+ return;
+ cgit_gopher_text_pad(show_date(t, tz, cgit_date_mode(DATE_ISO8601)), GOPHER_SUMMARY_DATE_LEN + 1);
+ return;
+}
+
+void cgit_print_http_headers(void)
+{
+ if (ctx.env.no_http && !strcmp(ctx.env.no_http, "1"))
+ return;
+
+ if (ctx.page.status)
+ htmlf("Status: %d %s\n", ctx.page.status, ctx.page.statusmsg);
+ if (ctx.page.mimetype && ctx.page.charset)
+ htmlf("Content-Type: %s; charset=%s\n", ctx.page.mimetype,
+ ctx.page.charset);
+ else if (ctx.page.mimetype)
+ htmlf("Content-Type: %s\n", ctx.page.mimetype);
+ if (ctx.page.size)
+ htmlf("Content-Length: %zd\n", ctx.page.size);
+ if (ctx.page.filename) {
+ html("Content-Disposition: inline; filename=\"");
+ html_header_arg_in_quotes(ctx.page.filename);
+ html("\"\n");
+ }
+ if (!ctx.env.authenticated)
+ html("Cache-Control: no-cache, no-store\n");
+ htmlf("Last-Modified: %s\n", http_date(ctx.page.modified));
+ htmlf("Expires: %s\n", http_date(ctx.page.expires));
+ if (ctx.page.etag)
+ htmlf("ETag: \"%s\"\n", ctx.page.etag);
+ html("\n");
+ if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
+ exit(0);
+}
+
+void cgit_redirect(const char *url, bool permanent)
+{
+ htmlf("Status: %d %s\n", permanent ? 301 : 302, permanent ? "Moved" : "Found");
+ html("Location: ");
+ html_url_path(url);
+ html("\n\n");
+}
+
+static void print_rel_vcs_link(const char *url)
+{
+ html("<link rel='vcs-git' href='");
+ html_attr(url);
+ html("' title='");
+ html_attr(ctx.repo->name);
+ html(" Git repository'/>\n");
+}
+
+void cgit_print_docstart(void)
+{
+ char *host = cgit_hosturl();
+
+ if (ctx.cfg.embedded) {
+ if (ctx.cfg.header)
+ html_include(ctx.cfg.header);
+ return;
+ }
+
+ html(cgit_doctype);
+ html("<html lang='en'>\n");
+ html("<head>\n");
+ html("<title>");
+ html_txt(ctx.page.title);
+ html("</title>\n");
+ htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version);
+ if (ctx.cfg.robots && *ctx.cfg.robots)
+ htmlf("<meta name='robots' content='%s'/>\n", ctx.cfg.robots);
+ html("<link rel='stylesheet' type='text/css' href='");
+ html_attr(ctx.cfg.css);
+ html("'/>\n");
+ if (ctx.cfg.favicon) {
+ html("<link rel='shortcut icon' href='");
+ html_attr(ctx.cfg.favicon);
+ html("'/>\n");
+ }
+ if (host && ctx.repo && ctx.qry.head) {
+ char *fileurl;
+ struct strbuf sb = STRBUF_INIT;
+ strbuf_addf(&sb, "h=%s", ctx.qry.head);
+
+ html("<link rel='alternate' title='Atom feed' href='");
+ html(cgit_httpscheme());
+ html_attr(host);
+ fileurl = cgit_fileurl(ctx.repo->url, "atom", ctx.qry.vpath,
+ sb.buf);
+ html_attr(fileurl);
+ html("' type='application/atom+xml'/>\n");
+ strbuf_release(&sb);
+ free(fileurl);
+ }
+ if (ctx.repo)
+ cgit_add_clone_urls(print_rel_vcs_link);
+ if (ctx.cfg.head_include)
+ html_include(ctx.cfg.head_include);
+ if (ctx.repo && ctx.repo->extra_head_content)
+ html(ctx.repo->extra_head_content);
+ html("</head>\n");
+ html("<body>\n");
+ if (ctx.cfg.header)
+ html_include(ctx.cfg.header);
+ free(host);
+}
+
+void cgit_print_docend(void)
+{
+ html("</div> <!-- class=content -->\n");
+ if (ctx.cfg.embedded) {
+ html("</div> <!-- id=cgit -->\n");
+ if (ctx.cfg.footer)
+ html_include(ctx.cfg.footer);
+ return;
+ }
+ if (ctx.cfg.footer)
+ html_include(ctx.cfg.footer);
+ else {
+ htmlf("<div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit %s</a> "
+ "(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string);
+ html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_ISO8601)));
+ html("</div>\n");
+ }
+ html("</div> <!-- id=cgit -->\n");
+ html("</body>\n</html>\n");
+}
+
+/*
+void cgit_print_error_page(int code, const char *msg, const char *fmt, ...)
+{
+ va_list ap;
+ ctx.page.expires = ctx.cfg.cache_dynamic_ttl;
+ ctx.page.status = code;
+ ctx.page.statusmsg = msg;
+ cgit_print_layout_start();
+ va_start(ap, fmt);
+ cgit_vprint_error(fmt, ap);
+ va_end(ap);
+ cgit_print_layout_end();
+}
+*/
+
+void cgit_print_layout_start(void)
+{
+ /*cgit_print_http_headers();
+ cgit_print_docstart();*/
+ cgit_print_pageheader();
+}
+
+void cgit_print_layout_end(void)
+{
+ /*cgit_print_docend();*/
+}
+
+static void add_clone_urls(void (*fn)(const char *), char *txt, char *suffix)
+{
+ struct strbuf **url_list = strbuf_split_str(txt, ' ', 0);
+ int i;
+
+ for (i = 0; url_list[i]; i++) {
+ strbuf_rtrim(url_list[i]);
+ if (url_list[i]->len == 0)
+ continue;
+ if (suffix && *suffix)
+ strbuf_addf(url_list[i], "/%s", suffix);
+ fn(url_list[i]->buf);
+ }
+
+ strbuf_list_free(url_list);
+}
+
+void cgit_add_clone_urls(void (*fn)(const char *))
+{
+ if (ctx.repo->clone_url)
+ add_clone_urls(fn, expand_macros(ctx.repo->clone_url), NULL);
+ else if (ctx.cfg.clone_prefix)
+ add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url);
+}
+
+static int print_branch_option(const char *refname, const struct object_id *oid,
+ int flags, void *cb_data)
+{
+ char *name = (char *)refname;
+ html_option(name, name, ctx.qry.head);
+ return 0;
+}
+
+void cgit_add_hidden_formfields(int incl_head, int incl_search,
+ const char *page)
+{
+ if (!ctx.cfg.virtual_root) {
+ struct strbuf url = STRBUF_INIT;
+
+ strbuf_addf(&url, "%s/%s", ctx.qry.repo, page);
+ if (ctx.qry.vpath)
+ strbuf_addf(&url, "/%s", ctx.qry.vpath);
+ html_hidden("url", url.buf);
+ strbuf_release(&url);
+ }
+
+ if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
+ strcmp(ctx.qry.head, ctx.repo->defbranch))
+ html_hidden("h", ctx.qry.head);
+
+ if (ctx.qry.sha1)
+ html_hidden("id", ctx.qry.sha1);
+ if (ctx.qry.sha2)
+ html_hidden("id2", ctx.qry.sha2);
+ if (ctx.qry.showmsg)
+ html_hidden("showmsg", "1");
+
+ if (incl_search) {
+ if (ctx.qry.grep)
+ html_hidden("qt", ctx.qry.grep);
+ if (ctx.qry.search)
+ html_hidden("q", ctx.qry.search);
+ }
+}
+
+static const char *hc(const char *page)
+{
+ if (!ctx.qry.page)
+ return NULL;
+
+ return strcmp(ctx.qry.page, page) ? NULL : "active";
+}
+
+static void cgit_print_path_crumbs(char *path)
+{
+ char *old_path = ctx.qry.path;
+ char *p = path, *q, *end = path + strlen(path);
+
+ ctx.qry.path = NULL;
+ cgit_self_link("root", NULL, NULL);
+ ctx.qry.path = p = path;
+ while (p < end) {
+ if (!(q = strchr(p, '/')))
+ q = end;
+ *q = '\0';
+ html_txt("/");
+ cgit_self_link(p, NULL, NULL);
+ if (q < end)
+ *q = '/';
+ p = q + 1;
+ }
+ ctx.qry.path = old_path;
+}
+
+
+
+
+static void print_header(void)
+{
+ if (ctx.repo) {
+ cgit_gopher_index_link("repo list", NULL, NULL, NULL, NULL, 0, 1);
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text(ctx.repo->name);
+ cgit_gopher_text(" (owned by ");
+ cgit_gopher_text(ctx.repo->owner);
+ cgit_gopher_text(")");
+ cgit_gopher_tab();
+ cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
+ cgit_gopher_end_selector();
+ } else
+ cgit_gopher_info(ctx.cfg.root_title);
+}
+
+void cgit_print_pageheader(void)
+{
+ if (!ctx.env.authenticated || !ctx.cfg.noheader)
+ print_header();
+
+ if (ctx.repo) {
+ if (ctx.repo->readme.nr)
+ reporevlink("about", "about", NULL,
+ hc("about"), ctx.qry.head, NULL,
+ NULL);
+ cgit_gopher_summary_link("summary", NULL, hc("summary"),
+ ctx.qry.head);
+ cgit_gopher_refs_link("refs", NULL, hc("refs"), ctx.qry.head,
+ ctx.qry.sha1, NULL);
+ cgit_gopher_log_link("log", NULL, hc("log"), ctx.qry.head,
+ NULL, ctx.qry.vpath, 0, NULL, NULL,
+ ctx.qry.showmsg, ctx.qry.follow);
+ if (ctx.qry.page && !strcmp(ctx.qry.page, "blame"))
+ cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head,
+ ctx.qry.sha1, ctx.qry.vpath);
+ else
+ cgit_gopher_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
+ ctx.qry.sha1, ctx.qry.vpath);
+ cgit_gopher_commit_link("commit", NULL, hc("commit"),
+ ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath);
+ cgit_gopher_diff_link("diff", NULL, hc("diff"), ctx.qry.head,
+ ctx.qry.sha1, ctx.qry.sha2, ctx.qry.vpath);
+ if (ctx.repo->max_stats)
+ cgit_stats_link("stats", NULL, hc("stats"),
+ ctx.qry.head, ctx.qry.vpath);
+ if (ctx.repo->homepage) {
+ /* cgit_gopher_menu("homepage", * ctx.repo->homepage);*/
+ }
+ } /*else {
+ char *currenturl = cgit_currenturl();
+ site_link(NULL, "index", NULL, hc("repolist"), NULL, NULL, 0, 1);
+ if (ctx.cfg.root_readme)
+ site_link("about", "about", NULL, hc("about"),
+ NULL, NULL, 0, 1);
+ html("</td><td class='form'>");
+ html("<form method='get' action='");
+ html_attr(currenturl);
+ html("'>\n");
+ html("<input type='search' name='q' size='10' value='");
+ html_attr(ctx.qry.search);
+ html("'/>\n");
+ html("<input type='submit' value='search'/>\n");
+ html("</form>");
+ free(currenturl);
+ }
+ if (ctx.repo && ctx.qry.vpath) {
+ html("<div class='path'>");
+ html("path: ");
+ cgit_print_path_crumbs(ctx.qry.vpath);
+ if (ctx.cfg.enable_follow_links && !strcmp(ctx.qry.page, "log")) {
+ html(" (");
+ ctx.qry.follow = !ctx.qry.follow;
+ cgit_self_link(ctx.qry.follow ? "follow" : "unfollow",
+ NULL, NULL);
+ ctx.qry.follow = !ctx.qry.follow;
+ html(")");
+ }
+ html("</div>");
+ }*/
+}
+
+void cgit_print_filemode(unsigned short mode)
+{
+ if (S_ISDIR(mode))
+ cgit_gopher_text("d");
+ else if (S_ISLNK(mode))
+ cgit_gopher_text("l");
+ else if (S_ISGITLINK(mode))
+ cgit_gopher_text("m");
+ else
+ cgit_gopher_text("-");
+ gopher_fileperm(mode >> 6);
+ gopher_fileperm(mode >> 3);
+ gopher_fileperm(mode);
+}
+
+void cgit_compose_snapshot_prefix(struct strbuf *filename, const char *base,
+ const char *ref)
+{
+ struct object_id oid;
+
+ /*
+ * Prettify snapshot names by stripping leading "v" or "V" if the tag
+ * name starts with {v,V}[0-9] and the prettify mapping is injective,
+ * i.e. each stripped tag can be inverted without ambiguities.
+ */
+ if (get_oid(fmt("refs/tags/%s", ref), &oid) == 0 &&
+ (ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]) &&
+ ((get_oid(fmt("refs/tags/%s", ref + 1), &oid) == 0) +
+ (get_oid(fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
+ (get_oid(fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1))
+ ref++;
+
+ strbuf_addf(filename, "%s-%s", base, ref);
+}
+
+void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref,
+ const char *separator)
+{
+ const struct cgit_snapshot_format *f;
+ struct strbuf filename = STRBUF_INIT;
+ const char *basename;
+ size_t prefixlen;
+
+ basename = cgit_snapshot_prefix(repo);
+ if (starts_with(ref, basename))
+ strbuf_addstr(&filename, ref);
+ else
+ cgit_compose_snapshot_prefix(&filename, basename, ref);
+
+ prefixlen = filename.len;
+ for (f = cgit_snapshot_formats; f->suffix; f++) {
+ if (!(repo->snapshots & cgit_snapshot_format_bit(f)))
+ continue;
+ strbuf_setlen(&filename, prefixlen);
+ strbuf_addstr(&filename, f->suffix);
+ cgit_snapshot_link(filename.buf, NULL, NULL, NULL, NULL,
+ filename.buf);
+ if (cgit_snapshot_get_sig(ref, f)) {
+ strbuf_addstr(&filename, ".asc");
+ html(" (");
+ cgit_snapshot_link("sig", NULL, NULL, NULL, NULL,
+ filename.buf);
+ html(")");
+ } else if (starts_with(f->suffix, ".tar") && cgit_snapshot_get_sig(ref, &cgit_snapshot_formats[0])) {
+ strbuf_setlen(&filename, strlen(filename.buf) - strlen(f->suffix));
+ strbuf_addstr(&filename, ".tar.asc");
+ html(" (");
+ cgit_snapshot_link("sig", NULL, NULL, NULL, NULL,
+ filename.buf);
+ html(")");
+ }
+ html(separator);
+ }
+ strbuf_release(&filename);
+}
+
+void cgit_set_title_from_path(const char *path)
+{
+ size_t path_len, path_index, path_last_end;
+ char *new_title;
+
+ if (!path)
+ return;
+
+ path_len = strlen(path);
+ new_title = xmalloc(path_len + 3 + strlen(ctx.page.title) + 1);
+ new_title[0] = '\0';
+
+ for (path_index = path_len, path_last_end = path_len; path_index-- > 0;) {
+ if (path[path_index] == '/') {
+ if (path_index == path_len - 1) {
+ path_last_end = path_index - 1;
+ continue;
+ }
+ strncat(new_title, &path[path_index + 1], path_last_end - path_index - 1);
+ strcat(new_title, "\\");
+ path_last_end = path_index;
+ }
+ }
+ if (path_last_end)
+ strncat(new_title, path, path_last_end);
+
+ strcat(new_title, " - ");
+ strcat(new_title, ctx.page.title);
+ ctx.page.title = new_title;
+
+}
diff --git a/ui_70-shared.h b/ui_70-shared.h
new file mode 100644
index 0000000..c793950
--- /dev/null
+++ b/ui_70-shared.h
@@ -0,0 +1,137 @@
+#ifndef UI_70_SHARED_H
+#define UI_70_SHARED_H
+
+#define GOPHER_TXT '0'
+#define GOPHER_MENU '1'
+#define GOPHER_CCSO '2'
+#define GOPHER_ERR '3'
+#define GOPHER_BINHX '4'
+#define GOPHER_DOS '5'
+#define GOPHER_UUENC '6'
+#define GOPHER_SRCH '7'
+#define GOPHER_TELN '8'
+#define GOPHER_BIN '9'
+#define GOPHER_IMG 'I'
+#define GOPHER_3270 'T'
+#define GOPHER_GIF 'g'
+#define GOPHER_HTML 'h'
+#define GOPHER_INFO 'i'
+#define GOPHER_SND 's'
+#define GOPHER_MIRR '+'
+
+#define GOPHER_SUMMARY_NAME_LEN 22
+#define GOPHER_SUMMARY_DESC_LEN 45
+#define GOPHER_SUMMARY_DATE_LEN 20
+#define GOPHER_SUMMARY_AUTH_LEN 20
+#define GOPHER_SUMMARY_AGE_LEN 10
+#define GOPHER_SUMMARY_MODE_LEN 12
+#define GOPHER_PAD_CHAR ' '
+
+
+
+void cgit_gopher_selector(char type, char *str, char *sel, const char *host, const char * port);
+void cgit_gopher_info(char *msg);
+void cgit_gopher_menu(char *descr, char *sel);
+void cgit_gopher_textfile(char *descr, char *sel);
+void cgit_gopher_error(char *descr);
+
+
+void cgit_gopher_start_selector(char type);
+void cgit_gopher_selector_descr(const char *descr);
+void cgit_gopher_selector_link(const char *sel);
+void cgit_gopher_text(const char *txt);
+void cgit_gopher_tab();
+void cgit_gopher_text_pad(const char *txt, int len);
+void cgit_gopher_end_selector();
+void gopherf(const char *format, ...);
+
+
+
+extern const char *cgit_httpscheme(void);
+extern char *cgit_hosturl(void);
+extern const char *cgit_rooturl(void);
+extern char *cgit_currenturl(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_gopher_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_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_70_SHARED_H */
+
diff --git a/ui_70-summary.c b/ui_70-summary.c
new file mode 100644
index 0000000..aa680f6
--- /dev/null
+++ b/ui_70-summary.c
@@ -0,0 +1,144 @@
+/* ui-summary.c: functions for generating repo summary page
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * 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><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;
+
+ cgit_print_layout_start();
+ if (ctx.repo->enable_log_filecount)
+ columns++;
+ if (ctx.repo->enable_log_linecount)
+ columns++;
+
+ cgit_print_branches(ctx.cfg.summary_branches);
+
+ cgit_print_tags(ctx.cfg.summary_tags);
+ if (ctx.cfg.summary_log > 0) {
+ cgit_print_log(ctx.qry.head, 0, ctx.cfg.summary_log, NULL,
+ NULL, NULL, 0, 0, 0);
+ }
+
+ urls = 0;
+}
+
+/* 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(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/ui_70-tag.c b/ui_70-tag.c
new file mode 100644
index 0000000..0a1b386
--- /dev/null
+++ b/ui_70-tag.c
@@ -0,0 +1,131 @@
+/* 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)
+ */
+
+#include "cgit.h"
+#include "ui-tag.h"
+#include "html.h"
+#include "ui_70-shared.h"
+
+static void print_tag_content(char *buf)
+{
+ char *p;
+
+ if (!buf)
+ return;
+
+ p = strchr(buf, '\n');
+ if (p)
+ *p = '\0';
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text(buf);
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+ if (p) {
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text(++p);
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+ }
+}
+
+static void print_download_links(char *revname)
+{
+ html("<tr><th>download</th><td class='sha1'>");
+ 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 (get_oid(fullref.buf, &oid)) {
+ cgit_gopher_error("Bad tag reference");
+ goto cleanup;
+ }
+ obj = parse_object(&oid);
+ if (!obj) {
+ cgit_gopher_error("Bad object id");
+ goto cleanup;
+ }
+ if (obj->type == OBJ_TAG) {
+ struct tag *tag;
+ struct taginfo *info;
+
+ tag = lookup_tag(&oid);
+ if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) {
+ cgit_gopher_error("Bad tag object");
+ goto cleanup;
+ }
+ cgit_print_layout_start();
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text_pad("tag name", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text(revname);
+ gopherf(" (%s)", oid_to_hex(&oid));
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+ if (info->tagger_date > 0) {
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text_pad("tag date", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text(show_date(info->tagger_date, info->tagger_tz,
+ cgit_date_mode(DATE_ISO8601)));
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+ }
+ if (info->tagger) {
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text_pad("tagged by", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text(info->tagger);
+ if (info->tagger_email && !ctx.cfg.noplainemail) {
+ cgit_gopher_text(" ");
+ cgit_gopher_text(info->tagger_email);
+ }
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+ }
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text_pad("tagged object", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text(oid_to_hex(&tag->tagged->oid));
+ cgit_gopher_tab();
+ cgit_object_link(tag->tagged);
+ cgit_gopher_end_selector();
+ print_tag_content(info->msg);
+ cgit_print_layout_end();
+ cgit_free_taginfo(info);
+ } else {
+ cgit_print_layout_start();
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text_pad("tag name", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text(revname);
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_gopher_text_pad("tagged object", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text(oid_to_hex(&obj->oid));
+ cgit_gopher_tab();
+ cgit_object_link(obj);
+ cgit_gopher_end_selector();
+ cgit_print_layout_end();
+ }
+
+cleanup:
+ strbuf_release(&fullref);
+}
diff --git a/ui_70-tree.c b/ui_70-tree.c
new file mode 100644
index 0000000..32fd585
--- /dev/null
+++ b/ui_70-tree.c
@@ -0,0 +1,337 @@
+/* ui-tree.c: functions for tree output
+ *
+ * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
+ * 2018 Vincenzo 'KatolaZ' Nicosia <katolaz@freaknet.org>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-tree.h"
+#include "html.h"
+#include "ui_70-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)
+{
+ cgit_gopher_text(buf);
+}
+
+#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, char *path, const char *basename, const char *rev)
+{
+ enum object_type type;
+ char *buf;
+ unsigned long size;
+
+ type = oid_object_info(the_repository, oid, &size);
+ if (type == OBJ_BAD) {
+ cgit_gopher_error("Bad object name");
+ return;
+ }
+
+ buf = read_object_file(oid, &type, &size);
+ if (!buf) {
+ cgit_gopher_error("Error reading object");
+ return;
+ }
+
+ cgit_set_title_from_path(path);
+
+ /*cgit_print_layout_start();*/
+
+ if (buffer_is_binary(buf, size))
+ 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, int stage,
+ 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(&tree_ctx.oid);
+ if (!tree)
+ return;
+
+ free(tree_ctx.name);
+ tree_ctx.name = NULL;
+ tree_ctx.count = 0;
+
+ read_tree_recursive(tree, "", 0, 1, &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, int stage, void *cbdata)
+{
+ struct walk_tree_context *walk_tree_ctx = cbdata;
+ char *name;
+ struct strbuf fullpath = STRBUF_INIT;
+ struct strbuf class = STRBUF_INIT;
+ enum object_type type;
+ unsigned long size = 0;
+
+ name = xstrdup(pathname);
+ strbuf_addf(&fullpath, "%s%s%s", ctx.qry.path ? ctx.qry.path : "",
+ ctx.qry.path ? "/" : "", name);
+
+ if (!S_ISGITLINK(mode)) {
+ type = oid_object_info(the_repository, oid, &size);
+ if (type == OBJ_BAD) {
+ cgit_gopher_error("Bad object");
+ free(name);
+ return 0;
+ }
+ }
+
+/*
+ if (S_ISGITLINK(mode)) {
+ cgit_submodule_link("ls-mod", fullpath.buf, oid_to_hex(oid));
+ } else
+*/
+ if (S_ISDIR(mode)) {
+ cgit_gopher_start_selector(GOPHER_MENU);
+ cgit_print_filemode(mode);
+ cgit_gopher_text(" ");
+ cgit_gopher_text_pad(name, GOPHER_SUMMARY_NAME_LEN);
+ gopherf("%10ld", size);
+ cgit_gopher_tab();
+ 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_gopher_start_selector(GOPHER_TXT);
+ cgit_print_filemode(mode);
+ cgit_gopher_text(" ");
+ cgit_gopher_text_pad(name, GOPHER_SUMMARY_NAME_LEN);
+ gopherf("%10ld", size);
+ cgit_gopher_tab();
+ cgit_tree_link(name, NULL, class.buf, ctx.qry.head,
+ walk_tree_ctx->curr_rev, fullpath.buf);
+ }
+/* 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.cfg.enable_blame)
+ cgit_blame_link("blame", NULL, "button", ctx.qry.head,
+ walk_tree_ctx->curr_rev, fullpath.buf);
+*/
+ cgit_gopher_end_selector();
+ free(name);
+ strbuf_release(&fullpath);
+ strbuf_release(&class);
+ return 0;
+}
+
+static void ls_head(void)
+{
+ cgit_print_layout_start();
+ cgit_gopher_start_selector(GOPHER_INFO);
+ cgit_gopher_text_pad("Mode", GOPHER_SUMMARY_MODE_LEN);
+ cgit_gopher_text_pad("Name", GOPHER_SUMMARY_NAME_LEN);
+ cgit_gopher_text_pad("Size", GOPHER_SUMMARY_AGE_LEN);
+ cgit_gopher_tab();
+ cgit_gopher_selector_link("Err");
+ cgit_gopher_end_selector();
+}
+
+static void ls_tail(void)
+{
+/* html("</table>\n");
+ cgit_print_layout_end();
+*/
+}
+
+static void ls_tree(const struct object_id *oid, 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_gopher_error("Not a tree object");
+ return;
+ }
+
+ ls_head();
+ read_tree_recursive(tree, "", 0, 1, &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, int stage, 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, stage, 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 (get_oid(rev, &oid)) {
+ cgit_gopher_error("Invalid revision name");
+ return;
+ }
+ commit = lookup_commit_reference(&oid);
+ if (!commit || parse_commit(commit)) {
+ cgit_gopher_error("Invalid commit reference");
+ return;
+ }
+
+ walk_tree_ctx.curr_rev = xstrdup(rev);
+
+ if (path == NULL) {
+ ls_tree(&commit->maybe_tree->object.oid, NULL, &walk_tree_ctx);
+ goto cleanup;
+ }
+
+ read_tree_recursive(commit->maybe_tree, "", 0, 0, &paths, walk_tree, &walk_tree_ctx);
+ if (walk_tree_ctx.state == 1)
+ ls_tail();
+ else if (walk_tree_ctx.state == 2)
+ ;
+ else
+ cgit_print_error_page(404, "Not found", "Path not found");
+
+cleanup:
+ free(walk_tree_ctx.curr_rev);
+}
diff --git a/ui_70_repolist.c b/ui_70_repolist.c
new file mode 100644
index 0000000..41424c0
--- /dev/null
+++ b/ui_70_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(char *path)
+{
+ time_t result;
+ size_t size;
+ char *buf = NULL;
+ struct strbuf date_buf = STRBUF_INIT;
+
+ if (readfile(path, &buf, &size)) {
+ free(buf);
+ return -1;
+ }
+
+ 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, ctx.repo->name, 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();
+}