summaryrefslogtreecommitdiffabout
authorLars Hjemli <hjemli@gmail.com>2008-12-06 16:38:19 (UTC)
committer Lars Hjemli <hjemli@gmail.com>2008-12-06 16:38:19 (UTC)
commitf86a23ff537258d36bf8f1876fa7a4bede6673d8 (patch) (unidiff)
tree8328d415416058cdc5b0fd2c6564ddcab5766c7a
parent140012d7a8e51df5a9f9c556696778b86ade4fc9 (diff)
downloadcgit-f86a23ff537258d36bf8f1876fa7a4bede6673d8.zip
cgit-f86a23ff537258d36bf8f1876fa7a4bede6673d8.tar.gz
cgit-f86a23ff537258d36bf8f1876fa7a4bede6673d8.tar.bz2
Add a 'stats' page to each repo
This new page, which is disabled by default, can be used to print some statistics about the number of commits per period in the repository, where period can be either weeks, months, quarters or years. The function can be activated globally by setting 'enable-stats=1' in cgitrc and disabled for individual repos by setting 'repo.enable-stats=0'. Signed-off-by: Lars Hjemli <hjemli@gmail.com>
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--Makefile1
-rw-r--r--cgit.c6
-rw-r--r--cgit.css77
-rw-r--r--cgit.h3
-rw-r--r--cgitrc.5.txt8
-rw-r--r--cmd.c10
-rw-r--r--shared.c1
-rw-r--r--ui-shared.c3
-rw-r--r--ui-stats.c380
-rw-r--r--ui-stats.h8
10 files changed, 497 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index 561af76..f426f98 100644
--- a/Makefile
+++ b/Makefile
@@ -1,129 +1,130 @@
1CGIT_VERSION = v0.8.1 1CGIT_VERSION = v0.8.1
2CGIT_SCRIPT_NAME = cgit.cgi 2CGIT_SCRIPT_NAME = cgit.cgi
3CGIT_SCRIPT_PATH = /var/www/htdocs/cgit 3CGIT_SCRIPT_PATH = /var/www/htdocs/cgit
4CGIT_CONFIG = /etc/cgitrc 4CGIT_CONFIG = /etc/cgitrc
5CACHE_ROOT = /var/cache/cgit 5CACHE_ROOT = /var/cache/cgit
6SHA1_HEADER = <openssl/sha.h> 6SHA1_HEADER = <openssl/sha.h>
7GIT_VER = 1.6.0.2 7GIT_VER = 1.6.0.2
8GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2 8GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2
9 9
10# 10#
11# Let the user override the above settings. 11# Let the user override the above settings.
12# 12#
13-include cgit.conf 13-include cgit.conf
14 14
15# 15#
16# Define a way to invoke make in subdirs quietly, shamelessly ripped 16# Define a way to invoke make in subdirs quietly, shamelessly ripped
17# from git.git 17# from git.git
18# 18#
19QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir 19QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir
20QUIET_SUBDIR1 = 20QUIET_SUBDIR1 =
21 21
22ifneq ($(findstring $(MAKEFLAGS),w),w) 22ifneq ($(findstring $(MAKEFLAGS),w),w)
23PRINT_DIR = --no-print-directory 23PRINT_DIR = --no-print-directory
24else # "make -w" 24else # "make -w"
25NO_SUBDIR = : 25NO_SUBDIR = :
26endif 26endif
27 27
28ifndef V 28ifndef V
29 QUIET_CC = @echo ' ' CC $@; 29 QUIET_CC = @echo ' ' CC $@;
30 QUIET_MM = @echo ' ' MM $@; 30 QUIET_MM = @echo ' ' MM $@;
31 QUIET_SUBDIR0 = +@subdir= 31 QUIET_SUBDIR0 = +@subdir=
32 QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \ 32 QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \
33 $(MAKE) $(PRINT_DIR) -C $$subdir 33 $(MAKE) $(PRINT_DIR) -C $$subdir
34endif 34endif
35 35
36# 36#
37# Define a pattern rule for automatic dependency building 37# Define a pattern rule for automatic dependency building
38# 38#
39%.d: %.c 39%.d: %.c
40 $(QUIET_MM)$(CC) $(CFLAGS) -MM $< | sed -e 's/\($*\)\.o:/\1.o $@:/g' >$@ 40 $(QUIET_MM)$(CC) $(CFLAGS) -MM $< | sed -e 's/\($*\)\.o:/\1.o $@:/g' >$@
41 41
42# 42#
43# Define a pattern rule for silent object building 43# Define a pattern rule for silent object building
44# 44#
45%.o: %.c 45%.o: %.c
46 $(QUIET_CC)$(CC) -o $*.o -c $(CFLAGS) $< 46 $(QUIET_CC)$(CC) -o $*.o -c $(CFLAGS) $<
47 47
48 48
49EXTLIBS = git/libgit.a git/xdiff/lib.a -lz -lcrypto 49EXTLIBS = git/libgit.a git/xdiff/lib.a -lz -lcrypto
50OBJECTS = 50OBJECTS =
51OBJECTS += cache.o 51OBJECTS += cache.o
52OBJECTS += cgit.o 52OBJECTS += cgit.o
53OBJECTS += cmd.o 53OBJECTS += cmd.o
54OBJECTS += configfile.o 54OBJECTS += configfile.o
55OBJECTS += html.o 55OBJECTS += html.o
56OBJECTS += parsing.o 56OBJECTS += parsing.o
57OBJECTS += scan-tree.o 57OBJECTS += scan-tree.o
58OBJECTS += shared.o 58OBJECTS += shared.o
59OBJECTS += ui-atom.o 59OBJECTS += ui-atom.o
60OBJECTS += ui-blob.o 60OBJECTS += ui-blob.o
61OBJECTS += ui-clone.o 61OBJECTS += ui-clone.o
62OBJECTS += ui-commit.o 62OBJECTS += ui-commit.o
63OBJECTS += ui-diff.o 63OBJECTS += ui-diff.o
64OBJECTS += ui-log.o 64OBJECTS += ui-log.o
65OBJECTS += ui-patch.o 65OBJECTS += ui-patch.o
66OBJECTS += ui-plain.o 66OBJECTS += ui-plain.o
67OBJECTS += ui-refs.o 67OBJECTS += ui-refs.o
68OBJECTS += ui-repolist.o 68OBJECTS += ui-repolist.o
69OBJECTS += ui-shared.o 69OBJECTS += ui-shared.o
70OBJECTS += ui-snapshot.o 70OBJECTS += ui-snapshot.o
71OBJECTS += ui-stats.o
71OBJECTS += ui-summary.o 72OBJECTS += ui-summary.o
72OBJECTS += ui-tag.o 73OBJECTS += ui-tag.o
73OBJECTS += ui-tree.o 74OBJECTS += ui-tree.o
74 75
75ifdef NEEDS_LIBICONV 76ifdef NEEDS_LIBICONV
76 EXTLIBS += -liconv 77 EXTLIBS += -liconv
77endif 78endif
78 79
79 80
80.PHONY: all libgit test install uninstall clean force-version get-git 81.PHONY: all libgit test install uninstall clean force-version get-git
81 82
82all: cgit 83all: cgit
83 84
84VERSION: force-version 85VERSION: force-version
85 @./gen-version.sh "$(CGIT_VERSION)" 86 @./gen-version.sh "$(CGIT_VERSION)"
86-include VERSION 87-include VERSION
87 88
88 89
89CFLAGS += -g -Wall -Igit 90CFLAGS += -g -Wall -Igit
90CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)' 91CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)'
91CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"' 92CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"'
92CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"' 93CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"'
93CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"' 94CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"'
94CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"' 95CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"'
95 96
96ifdef NO_ICONV 97ifdef NO_ICONV
97 CFLAGS += -DNO_ICONV 98 CFLAGS += -DNO_ICONV
98endif 99endif
99 100
100cgit: $(OBJECTS) libgit 101cgit: $(OBJECTS) libgit
101 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS) 102 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS)
102 103
103cgit.o: VERSION 104cgit.o: VERSION
104 105
105-include $(OBJECTS:.o=.d) 106-include $(OBJECTS:.o=.d)
106 107
107libgit: 108libgit:
108 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) libgit.a 109 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) libgit.a
109 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) xdiff/lib.a 110 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) xdiff/lib.a
110 111
111test: all 112test: all
112 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all 113 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all
113 114
114install: all 115install: all
115 mkdir -p $(DESTDIR)$(CGIT_SCRIPT_PATH) 116 mkdir -p $(DESTDIR)$(CGIT_SCRIPT_PATH)
116 install cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 117 install cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
117 install cgit.css $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.css 118 install cgit.css $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.css
118 install cgit.png $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.png 119 install cgit.png $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.png
119 120
120uninstall: 121uninstall:
121 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 122 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
122 rm -f $(CGIT_SCRIPT_PATH)/cgit.css 123 rm -f $(CGIT_SCRIPT_PATH)/cgit.css
123 rm -f $(CGIT_SCRIPT_PATH)/cgit.png 124 rm -f $(CGIT_SCRIPT_PATH)/cgit.png
124 125
125clean: 126clean:
126 rm -f cgit VERSION *.o *.d 127 rm -f cgit VERSION *.o *.d
127 128
128get-git: 129get-git:
129 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git 130 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git
diff --git a/cgit.c b/cgit.c
index c82587b..22b6d7c 100644
--- a/cgit.c
+++ b/cgit.c
@@ -1,472 +1,478 @@
1/* cgit.c: cgi for the git scm 1/* cgit.c: cgi for the git scm
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "cache.h" 10#include "cache.h"
11#include "cmd.h" 11#include "cmd.h"
12#include "configfile.h" 12#include "configfile.h"
13#include "html.h" 13#include "html.h"
14#include "ui-shared.h" 14#include "ui-shared.h"
15#include "scan-tree.h" 15#include "scan-tree.h"
16 16
17const char *cgit_version = CGIT_VERSION; 17const char *cgit_version = CGIT_VERSION;
18 18
19void config_cb(const char *name, const char *value) 19void config_cb(const char *name, const char *value)
20{ 20{
21 if (!strcmp(name, "root-title")) 21 if (!strcmp(name, "root-title"))
22 ctx.cfg.root_title = xstrdup(value); 22 ctx.cfg.root_title = xstrdup(value);
23 else if (!strcmp(name, "root-desc")) 23 else if (!strcmp(name, "root-desc"))
24 ctx.cfg.root_desc = xstrdup(value); 24 ctx.cfg.root_desc = xstrdup(value);
25 else if (!strcmp(name, "root-readme")) 25 else if (!strcmp(name, "root-readme"))
26 ctx.cfg.root_readme = xstrdup(value); 26 ctx.cfg.root_readme = xstrdup(value);
27 else if (!strcmp(name, "css")) 27 else if (!strcmp(name, "css"))
28 ctx.cfg.css = xstrdup(value); 28 ctx.cfg.css = xstrdup(value);
29 else if (!strcmp(name, "favicon")) 29 else if (!strcmp(name, "favicon"))
30 ctx.cfg.favicon = xstrdup(value); 30 ctx.cfg.favicon = xstrdup(value);
31 else if (!strcmp(name, "footer")) 31 else if (!strcmp(name, "footer"))
32 ctx.cfg.footer = xstrdup(value); 32 ctx.cfg.footer = xstrdup(value);
33 else if (!strcmp(name, "logo")) 33 else if (!strcmp(name, "logo"))
34 ctx.cfg.logo = xstrdup(value); 34 ctx.cfg.logo = xstrdup(value);
35 else if (!strcmp(name, "index-header")) 35 else if (!strcmp(name, "index-header"))
36 ctx.cfg.index_header = xstrdup(value); 36 ctx.cfg.index_header = xstrdup(value);
37 else if (!strcmp(name, "index-info")) 37 else if (!strcmp(name, "index-info"))
38 ctx.cfg.index_info = xstrdup(value); 38 ctx.cfg.index_info = xstrdup(value);
39 else if (!strcmp(name, "logo-link")) 39 else if (!strcmp(name, "logo-link"))
40 ctx.cfg.logo_link = xstrdup(value); 40 ctx.cfg.logo_link = xstrdup(value);
41 else if (!strcmp(name, "module-link")) 41 else if (!strcmp(name, "module-link"))
42 ctx.cfg.module_link = xstrdup(value); 42 ctx.cfg.module_link = xstrdup(value);
43 else if (!strcmp(name, "virtual-root")) { 43 else if (!strcmp(name, "virtual-root")) {
44 ctx.cfg.virtual_root = trim_end(value, '/'); 44 ctx.cfg.virtual_root = trim_end(value, '/');
45 if (!ctx.cfg.virtual_root && (!strcmp(value, "/"))) 45 if (!ctx.cfg.virtual_root && (!strcmp(value, "/")))
46 ctx.cfg.virtual_root = ""; 46 ctx.cfg.virtual_root = "";
47 } else if (!strcmp(name, "nocache")) 47 } else if (!strcmp(name, "nocache"))
48 ctx.cfg.nocache = atoi(value); 48 ctx.cfg.nocache = atoi(value);
49 else if (!strcmp(name, "snapshots")) 49 else if (!strcmp(name, "snapshots"))
50 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); 50 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
51 else if (!strcmp(name, "enable-index-links")) 51 else if (!strcmp(name, "enable-index-links"))
52 ctx.cfg.enable_index_links = atoi(value); 52 ctx.cfg.enable_index_links = atoi(value);
53 else if (!strcmp(name, "enable-log-filecount")) 53 else if (!strcmp(name, "enable-log-filecount"))
54 ctx.cfg.enable_log_filecount = atoi(value); 54 ctx.cfg.enable_log_filecount = atoi(value);
55 else if (!strcmp(name, "enable-log-linecount")) 55 else if (!strcmp(name, "enable-log-linecount"))
56 ctx.cfg.enable_log_linecount = atoi(value); 56 ctx.cfg.enable_log_linecount = atoi(value);
57 else if (!strcmp(name, "enable-stats"))
58 ctx.cfg.enable_stats = atoi(value);
57 else if (!strcmp(name, "cache-size")) 59 else if (!strcmp(name, "cache-size"))
58 ctx.cfg.cache_size = atoi(value); 60 ctx.cfg.cache_size = atoi(value);
59 else if (!strcmp(name, "cache-root")) 61 else if (!strcmp(name, "cache-root"))
60 ctx.cfg.cache_root = xstrdup(value); 62 ctx.cfg.cache_root = xstrdup(value);
61 else if (!strcmp(name, "cache-root-ttl")) 63 else if (!strcmp(name, "cache-root-ttl"))
62 ctx.cfg.cache_root_ttl = atoi(value); 64 ctx.cfg.cache_root_ttl = atoi(value);
63 else if (!strcmp(name, "cache-repo-ttl")) 65 else if (!strcmp(name, "cache-repo-ttl"))
64 ctx.cfg.cache_repo_ttl = atoi(value); 66 ctx.cfg.cache_repo_ttl = atoi(value);
65 else if (!strcmp(name, "cache-static-ttl")) 67 else if (!strcmp(name, "cache-static-ttl"))
66 ctx.cfg.cache_static_ttl = atoi(value); 68 ctx.cfg.cache_static_ttl = atoi(value);
67 else if (!strcmp(name, "cache-dynamic-ttl")) 69 else if (!strcmp(name, "cache-dynamic-ttl"))
68 ctx.cfg.cache_dynamic_ttl = atoi(value); 70 ctx.cfg.cache_dynamic_ttl = atoi(value);
69 else if (!strcmp(name, "max-message-length")) 71 else if (!strcmp(name, "max-message-length"))
70 ctx.cfg.max_msg_len = atoi(value); 72 ctx.cfg.max_msg_len = atoi(value);
71 else if (!strcmp(name, "max-repodesc-length")) 73 else if (!strcmp(name, "max-repodesc-length"))
72 ctx.cfg.max_repodesc_len = atoi(value); 74 ctx.cfg.max_repodesc_len = atoi(value);
73 else if (!strcmp(name, "max-repo-count")) 75 else if (!strcmp(name, "max-repo-count"))
74 ctx.cfg.max_repo_count = atoi(value); 76 ctx.cfg.max_repo_count = atoi(value);
75 else if (!strcmp(name, "max-commit-count")) 77 else if (!strcmp(name, "max-commit-count"))
76 ctx.cfg.max_commit_count = atoi(value); 78 ctx.cfg.max_commit_count = atoi(value);
77 else if (!strcmp(name, "summary-log")) 79 else if (!strcmp(name, "summary-log"))
78 ctx.cfg.summary_log = atoi(value); 80 ctx.cfg.summary_log = atoi(value);
79 else if (!strcmp(name, "summary-branches")) 81 else if (!strcmp(name, "summary-branches"))
80 ctx.cfg.summary_branches = atoi(value); 82 ctx.cfg.summary_branches = atoi(value);
81 else if (!strcmp(name, "summary-tags")) 83 else if (!strcmp(name, "summary-tags"))
82 ctx.cfg.summary_tags = atoi(value); 84 ctx.cfg.summary_tags = atoi(value);
83 else if (!strcmp(name, "agefile")) 85 else if (!strcmp(name, "agefile"))
84 ctx.cfg.agefile = xstrdup(value); 86 ctx.cfg.agefile = xstrdup(value);
85 else if (!strcmp(name, "renamelimit")) 87 else if (!strcmp(name, "renamelimit"))
86 ctx.cfg.renamelimit = atoi(value); 88 ctx.cfg.renamelimit = atoi(value);
87 else if (!strcmp(name, "robots")) 89 else if (!strcmp(name, "robots"))
88 ctx.cfg.robots = xstrdup(value); 90 ctx.cfg.robots = xstrdup(value);
89 else if (!strcmp(name, "clone-prefix")) 91 else if (!strcmp(name, "clone-prefix"))
90 ctx.cfg.clone_prefix = xstrdup(value); 92 ctx.cfg.clone_prefix = xstrdup(value);
91 else if (!strcmp(name, "local-time")) 93 else if (!strcmp(name, "local-time"))
92 ctx.cfg.local_time = atoi(value); 94 ctx.cfg.local_time = atoi(value);
93 else if (!strcmp(name, "repo.group")) 95 else if (!strcmp(name, "repo.group"))
94 ctx.cfg.repo_group = xstrdup(value); 96 ctx.cfg.repo_group = xstrdup(value);
95 else if (!strcmp(name, "repo.url")) 97 else if (!strcmp(name, "repo.url"))
96 ctx.repo = cgit_add_repo(value); 98 ctx.repo = cgit_add_repo(value);
97 else if (!strcmp(name, "repo.name")) 99 else if (!strcmp(name, "repo.name"))
98 ctx.repo->name = xstrdup(value); 100 ctx.repo->name = xstrdup(value);
99 else if (ctx.repo && !strcmp(name, "repo.path")) 101 else if (ctx.repo && !strcmp(name, "repo.path"))
100 ctx.repo->path = trim_end(value, '/'); 102 ctx.repo->path = trim_end(value, '/');
101 else if (ctx.repo && !strcmp(name, "repo.clone-url")) 103 else if (ctx.repo && !strcmp(name, "repo.clone-url"))
102 ctx.repo->clone_url = xstrdup(value); 104 ctx.repo->clone_url = xstrdup(value);
103 else if (ctx.repo && !strcmp(name, "repo.desc")) 105 else if (ctx.repo && !strcmp(name, "repo.desc"))
104 ctx.repo->desc = xstrdup(value); 106 ctx.repo->desc = xstrdup(value);
105 else if (ctx.repo && !strcmp(name, "repo.owner")) 107 else if (ctx.repo && !strcmp(name, "repo.owner"))
106 ctx.repo->owner = xstrdup(value); 108 ctx.repo->owner = xstrdup(value);
107 else if (ctx.repo && !strcmp(name, "repo.defbranch")) 109 else if (ctx.repo && !strcmp(name, "repo.defbranch"))
108 ctx.repo->defbranch = xstrdup(value); 110 ctx.repo->defbranch = xstrdup(value);
109 else if (ctx.repo && !strcmp(name, "repo.snapshots")) 111 else if (ctx.repo && !strcmp(name, "repo.snapshots"))
110 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */ 112 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */
111 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount")) 113 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount"))
112 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value); 114 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
113 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount")) 115 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
114 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value); 116 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
117 else if (ctx.repo && !strcmp(name, "repo.enable-stats"))
118 ctx.repo->enable_stats = ctx.cfg.enable_stats && atoi(value);
115 else if (ctx.repo && !strcmp(name, "repo.module-link")) 119 else if (ctx.repo && !strcmp(name, "repo.module-link"))
116 ctx.repo->module_link= xstrdup(value); 120 ctx.repo->module_link= xstrdup(value);
117 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) { 121 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
118 if (*value == '/') 122 if (*value == '/')
119 ctx.repo->readme = xstrdup(value); 123 ctx.repo->readme = xstrdup(value);
120 else 124 else
121 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value)); 125 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
122 } else if (!strcmp(name, "include")) 126 } else if (!strcmp(name, "include"))
123 parse_configfile(value, config_cb); 127 parse_configfile(value, config_cb);
124} 128}
125 129
126static void querystring_cb(const char *name, const char *value) 130static void querystring_cb(const char *name, const char *value)
127{ 131{
128 if (!strcmp(name,"r")) { 132 if (!strcmp(name,"r")) {
129 ctx.qry.repo = xstrdup(value); 133 ctx.qry.repo = xstrdup(value);
130 ctx.repo = cgit_get_repoinfo(value); 134 ctx.repo = cgit_get_repoinfo(value);
131 } else if (!strcmp(name, "p")) { 135 } else if (!strcmp(name, "p")) {
132 ctx.qry.page = xstrdup(value); 136 ctx.qry.page = xstrdup(value);
133 } else if (!strcmp(name, "url")) { 137 } else if (!strcmp(name, "url")) {
134 ctx.qry.url = xstrdup(value); 138 ctx.qry.url = xstrdup(value);
135 cgit_parse_url(value); 139 cgit_parse_url(value);
136 } else if (!strcmp(name, "qt")) { 140 } else if (!strcmp(name, "qt")) {
137 ctx.qry.grep = xstrdup(value); 141 ctx.qry.grep = xstrdup(value);
138 } else if (!strcmp(name, "q")) { 142 } else if (!strcmp(name, "q")) {
139 ctx.qry.search = xstrdup(value); 143 ctx.qry.search = xstrdup(value);
140 } else if (!strcmp(name, "h")) { 144 } else if (!strcmp(name, "h")) {
141 ctx.qry.head = xstrdup(value); 145 ctx.qry.head = xstrdup(value);
142 ctx.qry.has_symref = 1; 146 ctx.qry.has_symref = 1;
143 } else if (!strcmp(name, "id")) { 147 } else if (!strcmp(name, "id")) {
144 ctx.qry.sha1 = xstrdup(value); 148 ctx.qry.sha1 = xstrdup(value);
145 ctx.qry.has_sha1 = 1; 149 ctx.qry.has_sha1 = 1;
146 } else if (!strcmp(name, "id2")) { 150 } else if (!strcmp(name, "id2")) {
147 ctx.qry.sha2 = xstrdup(value); 151 ctx.qry.sha2 = xstrdup(value);
148 ctx.qry.has_sha1 = 1; 152 ctx.qry.has_sha1 = 1;
149 } else if (!strcmp(name, "ofs")) { 153 } else if (!strcmp(name, "ofs")) {
150 ctx.qry.ofs = atoi(value); 154 ctx.qry.ofs = atoi(value);
151 } else if (!strcmp(name, "path")) { 155 } else if (!strcmp(name, "path")) {
152 ctx.qry.path = trim_end(value, '/'); 156 ctx.qry.path = trim_end(value, '/');
153 } else if (!strcmp(name, "name")) { 157 } else if (!strcmp(name, "name")) {
154 ctx.qry.name = xstrdup(value); 158 ctx.qry.name = xstrdup(value);
155 } else if (!strcmp(name, "mimetype")) { 159 } else if (!strcmp(name, "mimetype")) {
156 ctx.qry.mimetype = xstrdup(value); 160 ctx.qry.mimetype = xstrdup(value);
161 } else if (!strcmp(name, "period")) {
162 ctx.qry.period = xstrdup(value);
157 } 163 }
158} 164}
159 165
160static void prepare_context(struct cgit_context *ctx) 166static void prepare_context(struct cgit_context *ctx)
161{ 167{
162 memset(ctx, 0, sizeof(ctx)); 168 memset(ctx, 0, sizeof(ctx));
163 ctx->cfg.agefile = "info/web/last-modified"; 169 ctx->cfg.agefile = "info/web/last-modified";
164 ctx->cfg.nocache = 0; 170 ctx->cfg.nocache = 0;
165 ctx->cfg.cache_size = 0; 171 ctx->cfg.cache_size = 0;
166 ctx->cfg.cache_dynamic_ttl = 5; 172 ctx->cfg.cache_dynamic_ttl = 5;
167 ctx->cfg.cache_max_create_time = 5; 173 ctx->cfg.cache_max_create_time = 5;
168 ctx->cfg.cache_repo_ttl = 5; 174 ctx->cfg.cache_repo_ttl = 5;
169 ctx->cfg.cache_root = CGIT_CACHE_ROOT; 175 ctx->cfg.cache_root = CGIT_CACHE_ROOT;
170 ctx->cfg.cache_root_ttl = 5; 176 ctx->cfg.cache_root_ttl = 5;
171 ctx->cfg.cache_static_ttl = -1; 177 ctx->cfg.cache_static_ttl = -1;
172 ctx->cfg.css = "/cgit.css"; 178 ctx->cfg.css = "/cgit.css";
173 ctx->cfg.logo = "/git-logo.png"; 179 ctx->cfg.logo = "/git-logo.png";
174 ctx->cfg.local_time = 0; 180 ctx->cfg.local_time = 0;
175 ctx->cfg.max_repo_count = 50; 181 ctx->cfg.max_repo_count = 50;
176 ctx->cfg.max_commit_count = 50; 182 ctx->cfg.max_commit_count = 50;
177 ctx->cfg.max_lock_attempts = 5; 183 ctx->cfg.max_lock_attempts = 5;
178 ctx->cfg.max_msg_len = 80; 184 ctx->cfg.max_msg_len = 80;
179 ctx->cfg.max_repodesc_len = 80; 185 ctx->cfg.max_repodesc_len = 80;
180 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; 186 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s";
181 ctx->cfg.renamelimit = -1; 187 ctx->cfg.renamelimit = -1;
182 ctx->cfg.robots = "index, nofollow"; 188 ctx->cfg.robots = "index, nofollow";
183 ctx->cfg.root_title = "Git repository browser"; 189 ctx->cfg.root_title = "Git repository browser";
184 ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 190 ctx->cfg.root_desc = "a fast webinterface for the git dscm";
185 ctx->cfg.script_name = CGIT_SCRIPT_NAME; 191 ctx->cfg.script_name = CGIT_SCRIPT_NAME;
186 ctx->cfg.summary_branches = 10; 192 ctx->cfg.summary_branches = 10;
187 ctx->cfg.summary_log = 10; 193 ctx->cfg.summary_log = 10;
188 ctx->cfg.summary_tags = 10; 194 ctx->cfg.summary_tags = 10;
189 ctx->page.mimetype = "text/html"; 195 ctx->page.mimetype = "text/html";
190 ctx->page.charset = PAGE_ENCODING; 196 ctx->page.charset = PAGE_ENCODING;
191 ctx->page.filename = NULL; 197 ctx->page.filename = NULL;
192 ctx->page.size = 0; 198 ctx->page.size = 0;
193 ctx->page.modified = time(NULL); 199 ctx->page.modified = time(NULL);
194 ctx->page.expires = ctx->page.modified; 200 ctx->page.expires = ctx->page.modified;
195} 201}
196 202
197struct refmatch { 203struct refmatch {
198 char *req_ref; 204 char *req_ref;
199 char *first_ref; 205 char *first_ref;
200 int match; 206 int match;
201}; 207};
202 208
203int find_current_ref(const char *refname, const unsigned char *sha1, 209int find_current_ref(const char *refname, const unsigned char *sha1,
204 int flags, void *cb_data) 210 int flags, void *cb_data)
205{ 211{
206 struct refmatch *info; 212 struct refmatch *info;
207 213
208 info = (struct refmatch *)cb_data; 214 info = (struct refmatch *)cb_data;
209 if (!strcmp(refname, info->req_ref)) 215 if (!strcmp(refname, info->req_ref))
210 info->match = 1; 216 info->match = 1;
211 if (!info->first_ref) 217 if (!info->first_ref)
212 info->first_ref = xstrdup(refname); 218 info->first_ref = xstrdup(refname);
213 return info->match; 219 return info->match;
214} 220}
215 221
216char *find_default_branch(struct cgit_repo *repo) 222char *find_default_branch(struct cgit_repo *repo)
217{ 223{
218 struct refmatch info; 224 struct refmatch info;
219 char *ref; 225 char *ref;
220 226
221 info.req_ref = repo->defbranch; 227 info.req_ref = repo->defbranch;
222 info.first_ref = NULL; 228 info.first_ref = NULL;
223 info.match = 0; 229 info.match = 0;
224 for_each_branch_ref(find_current_ref, &info); 230 for_each_branch_ref(find_current_ref, &info);
225 if (info.match) 231 if (info.match)
226 ref = info.req_ref; 232 ref = info.req_ref;
227 else 233 else
228 ref = info.first_ref; 234 ref = info.first_ref;
229 if (ref) 235 if (ref)
230 ref = xstrdup(ref); 236 ref = xstrdup(ref);
231 return ref; 237 return ref;
232} 238}
233 239
234static int prepare_repo_cmd(struct cgit_context *ctx) 240static int prepare_repo_cmd(struct cgit_context *ctx)
235{ 241{
236 char *tmp; 242 char *tmp;
237 unsigned char sha1[20]; 243 unsigned char sha1[20];
238 int nongit = 0; 244 int nongit = 0;
239 245
240 setenv("GIT_DIR", ctx->repo->path, 1); 246 setenv("GIT_DIR", ctx->repo->path, 1);
241 setup_git_directory_gently(&nongit); 247 setup_git_directory_gently(&nongit);
242 if (nongit) { 248 if (nongit) {
243 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title, 249 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title,
244 "config error"); 250 "config error");
245 tmp = fmt("Not a git repository: '%s'", ctx->repo->path); 251 tmp = fmt("Not a git repository: '%s'", ctx->repo->path);
246 ctx->repo = NULL; 252 ctx->repo = NULL;
247 cgit_print_http_headers(ctx); 253 cgit_print_http_headers(ctx);
248 cgit_print_docstart(ctx); 254 cgit_print_docstart(ctx);
249 cgit_print_pageheader(ctx); 255 cgit_print_pageheader(ctx);
250 cgit_print_error(tmp); 256 cgit_print_error(tmp);
251 cgit_print_docend(); 257 cgit_print_docend();
252 return 1; 258 return 1;
253 } 259 }
254 ctx->page.title = fmt("%s - %s", ctx->repo->name, ctx->repo->desc); 260 ctx->page.title = fmt("%s - %s", ctx->repo->name, ctx->repo->desc);
255 261
256 if (!ctx->qry.head) { 262 if (!ctx->qry.head) {
257 ctx->qry.nohead = 1; 263 ctx->qry.nohead = 1;
258 ctx->qry.head = find_default_branch(ctx->repo); 264 ctx->qry.head = find_default_branch(ctx->repo);
259 ctx->repo->defbranch = ctx->qry.head; 265 ctx->repo->defbranch = ctx->qry.head;
260 } 266 }
261 267
262 if (!ctx->qry.head) { 268 if (!ctx->qry.head) {
263 cgit_print_http_headers(ctx); 269 cgit_print_http_headers(ctx);
264 cgit_print_docstart(ctx); 270 cgit_print_docstart(ctx);
265 cgit_print_pageheader(ctx); 271 cgit_print_pageheader(ctx);
266 cgit_print_error("Repository seems to be empty"); 272 cgit_print_error("Repository seems to be empty");
267 cgit_print_docend(); 273 cgit_print_docend();
268 return 1; 274 return 1;
269 } 275 }
270 276
271 if (get_sha1(ctx->qry.head, sha1)) { 277 if (get_sha1(ctx->qry.head, sha1)) {
272 tmp = xstrdup(ctx->qry.head); 278 tmp = xstrdup(ctx->qry.head);
273 ctx->qry.head = ctx->repo->defbranch; 279 ctx->qry.head = ctx->repo->defbranch;
274 cgit_print_http_headers(ctx); 280 cgit_print_http_headers(ctx);
275 cgit_print_docstart(ctx); 281 cgit_print_docstart(ctx);
276 cgit_print_pageheader(ctx); 282 cgit_print_pageheader(ctx);
277 cgit_print_error(fmt("Invalid branch: %s", tmp)); 283 cgit_print_error(fmt("Invalid branch: %s", tmp));
278 cgit_print_docend(); 284 cgit_print_docend();
279 return 1; 285 return 1;
280 } 286 }
281 return 0; 287 return 0;
282} 288}
283 289
284static void process_request(void *cbdata) 290static void process_request(void *cbdata)
285{ 291{
286 struct cgit_context *ctx = cbdata; 292 struct cgit_context *ctx = cbdata;
287 struct cgit_cmd *cmd; 293 struct cgit_cmd *cmd;
288 294
289 cmd = cgit_get_cmd(ctx); 295 cmd = cgit_get_cmd(ctx);
290 if (!cmd) { 296 if (!cmd) {
291 ctx->page.title = "cgit error"; 297 ctx->page.title = "cgit error";
292 ctx->repo = NULL; 298 ctx->repo = NULL;
293 cgit_print_http_headers(ctx); 299 cgit_print_http_headers(ctx);
294 cgit_print_docstart(ctx); 300 cgit_print_docstart(ctx);
295 cgit_print_pageheader(ctx); 301 cgit_print_pageheader(ctx);
296 cgit_print_error("Invalid request"); 302 cgit_print_error("Invalid request");
297 cgit_print_docend(); 303 cgit_print_docend();
298 return; 304 return;
299 } 305 }
300 306
301 if (cmd->want_repo && !ctx->repo) { 307 if (cmd->want_repo && !ctx->repo) {
302 cgit_print_http_headers(ctx); 308 cgit_print_http_headers(ctx);
303 cgit_print_docstart(ctx); 309 cgit_print_docstart(ctx);
304 cgit_print_pageheader(ctx); 310 cgit_print_pageheader(ctx);
305 cgit_print_error(fmt("No repository selected")); 311 cgit_print_error(fmt("No repository selected"));
306 cgit_print_docend(); 312 cgit_print_docend();
307 return; 313 return;
308 } 314 }
309 315
310 if (ctx->repo && prepare_repo_cmd(ctx)) 316 if (ctx->repo && prepare_repo_cmd(ctx))
311 return; 317 return;
312 318
313 if (cmd->want_layout) { 319 if (cmd->want_layout) {
314 cgit_print_http_headers(ctx); 320 cgit_print_http_headers(ctx);
315 cgit_print_docstart(ctx); 321 cgit_print_docstart(ctx);
316 cgit_print_pageheader(ctx); 322 cgit_print_pageheader(ctx);
317 } 323 }
318 324
319 cmd->fn(ctx); 325 cmd->fn(ctx);
320 326
321 if (cmd->want_layout) 327 if (cmd->want_layout)
322 cgit_print_docend(); 328 cgit_print_docend();
323} 329}
324 330
325int cmp_repos(const void *a, const void *b) 331int cmp_repos(const void *a, const void *b)
326{ 332{
327 const struct cgit_repo *ra = a, *rb = b; 333 const struct cgit_repo *ra = a, *rb = b;
328 return strcmp(ra->url, rb->url); 334 return strcmp(ra->url, rb->url);
329} 335}
330 336
331void print_repo(struct cgit_repo *repo) 337void print_repo(struct cgit_repo *repo)
332{ 338{
333 printf("repo.url=%s\n", repo->url); 339 printf("repo.url=%s\n", repo->url);
334 printf("repo.name=%s\n", repo->name); 340 printf("repo.name=%s\n", repo->name);
335 printf("repo.path=%s\n", repo->path); 341 printf("repo.path=%s\n", repo->path);
336 if (repo->owner) 342 if (repo->owner)
337 printf("repo.owner=%s\n", repo->owner); 343 printf("repo.owner=%s\n", repo->owner);
338 if (repo->desc) 344 if (repo->desc)
339 printf("repo.desc=%s\n", repo->desc); 345 printf("repo.desc=%s\n", repo->desc);
340 if (repo->readme) 346 if (repo->readme)
341 printf("repo.readme=%s\n", repo->readme); 347 printf("repo.readme=%s\n", repo->readme);
342 printf("\n"); 348 printf("\n");
343} 349}
344 350
345void print_repolist(struct cgit_repolist *list) 351void print_repolist(struct cgit_repolist *list)
346{ 352{
347 int i; 353 int i;
348 354
349 for(i = 0; i < list->count; i++) 355 for(i = 0; i < list->count; i++)
350 print_repo(&list->repos[i]); 356 print_repo(&list->repos[i]);
351} 357}
352 358
353 359
354static void cgit_parse_args(int argc, const char **argv) 360static void cgit_parse_args(int argc, const char **argv)
355{ 361{
356 int i; 362 int i;
357 int scan = 0; 363 int scan = 0;
358 364
359 for (i = 1; i < argc; i++) { 365 for (i = 1; i < argc; i++) {
360 if (!strncmp(argv[i], "--cache=", 8)) { 366 if (!strncmp(argv[i], "--cache=", 8)) {
361 ctx.cfg.cache_root = xstrdup(argv[i]+8); 367 ctx.cfg.cache_root = xstrdup(argv[i]+8);
362 } 368 }
363 if (!strcmp(argv[i], "--nocache")) { 369 if (!strcmp(argv[i], "--nocache")) {
364 ctx.cfg.nocache = 1; 370 ctx.cfg.nocache = 1;
365 } 371 }
366 if (!strncmp(argv[i], "--query=", 8)) { 372 if (!strncmp(argv[i], "--query=", 8)) {
367 ctx.qry.raw = xstrdup(argv[i]+8); 373 ctx.qry.raw = xstrdup(argv[i]+8);
368 } 374 }
369 if (!strncmp(argv[i], "--repo=", 7)) { 375 if (!strncmp(argv[i], "--repo=", 7)) {
370 ctx.qry.repo = xstrdup(argv[i]+7); 376 ctx.qry.repo = xstrdup(argv[i]+7);
371 } 377 }
372 if (!strncmp(argv[i], "--page=", 7)) { 378 if (!strncmp(argv[i], "--page=", 7)) {
373 ctx.qry.page = xstrdup(argv[i]+7); 379 ctx.qry.page = xstrdup(argv[i]+7);
374 } 380 }
375 if (!strncmp(argv[i], "--head=", 7)) { 381 if (!strncmp(argv[i], "--head=", 7)) {
376 ctx.qry.head = xstrdup(argv[i]+7); 382 ctx.qry.head = xstrdup(argv[i]+7);
377 ctx.qry.has_symref = 1; 383 ctx.qry.has_symref = 1;
378 } 384 }
379 if (!strncmp(argv[i], "--sha1=", 7)) { 385 if (!strncmp(argv[i], "--sha1=", 7)) {
380 ctx.qry.sha1 = xstrdup(argv[i]+7); 386 ctx.qry.sha1 = xstrdup(argv[i]+7);
381 ctx.qry.has_sha1 = 1; 387 ctx.qry.has_sha1 = 1;
382 } 388 }
383 if (!strncmp(argv[i], "--ofs=", 6)) { 389 if (!strncmp(argv[i], "--ofs=", 6)) {
384 ctx.qry.ofs = atoi(argv[i]+6); 390 ctx.qry.ofs = atoi(argv[i]+6);
385 } 391 }
386 if (!strncmp(argv[i], "--scan-tree=", 12)) { 392 if (!strncmp(argv[i], "--scan-tree=", 12)) {
387 scan++; 393 scan++;
388 scan_tree(argv[i] + 12); 394 scan_tree(argv[i] + 12);
389 } 395 }
390 } 396 }
391 if (scan) { 397 if (scan) {
392 qsort(cgit_repolist.repos, cgit_repolist.count, 398 qsort(cgit_repolist.repos, cgit_repolist.count,
393 sizeof(struct cgit_repo), cmp_repos); 399 sizeof(struct cgit_repo), cmp_repos);
394 print_repolist(&cgit_repolist); 400 print_repolist(&cgit_repolist);
395 exit(0); 401 exit(0);
396 } 402 }
397} 403}
398 404
399static int calc_ttl() 405static int calc_ttl()
400{ 406{
401 if (!ctx.repo) 407 if (!ctx.repo)
402 return ctx.cfg.cache_root_ttl; 408 return ctx.cfg.cache_root_ttl;
403 409
404 if (!ctx.qry.page) 410 if (!ctx.qry.page)
405 return ctx.cfg.cache_repo_ttl; 411 return ctx.cfg.cache_repo_ttl;
406 412
407 if (ctx.qry.has_symref) 413 if (ctx.qry.has_symref)
408 return ctx.cfg.cache_dynamic_ttl; 414 return ctx.cfg.cache_dynamic_ttl;
409 415
410 if (ctx.qry.has_sha1) 416 if (ctx.qry.has_sha1)
411 return ctx.cfg.cache_static_ttl; 417 return ctx.cfg.cache_static_ttl;
412 418
413 return ctx.cfg.cache_repo_ttl; 419 return ctx.cfg.cache_repo_ttl;
414} 420}
415 421
416int main(int argc, const char **argv) 422int main(int argc, const char **argv)
417{ 423{
418 const char *cgit_config_env = getenv("CGIT_CONFIG"); 424 const char *cgit_config_env = getenv("CGIT_CONFIG");
419 const char *path; 425 const char *path;
420 char *qry; 426 char *qry;
421 int err, ttl; 427 int err, ttl;
422 428
423 prepare_context(&ctx); 429 prepare_context(&ctx);
424 cgit_repolist.length = 0; 430 cgit_repolist.length = 0;
425 cgit_repolist.count = 0; 431 cgit_repolist.count = 0;
426 cgit_repolist.repos = NULL; 432 cgit_repolist.repos = NULL;
427 433
428 if (getenv("SCRIPT_NAME")) 434 if (getenv("SCRIPT_NAME"))
429 ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME")); 435 ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME"));
430 if (getenv("QUERY_STRING")) 436 if (getenv("QUERY_STRING"))
431 ctx.qry.raw = xstrdup(getenv("QUERY_STRING")); 437 ctx.qry.raw = xstrdup(getenv("QUERY_STRING"));
432 cgit_parse_args(argc, argv); 438 cgit_parse_args(argc, argv);
433 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG, 439 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG,
434 config_cb); 440 config_cb);
435 ctx.repo = NULL; 441 ctx.repo = NULL;
436 http_parse_querystring(ctx.qry.raw, querystring_cb); 442 http_parse_querystring(ctx.qry.raw, querystring_cb);
437 443
438 /* If virtual-root isn't specified in cgitrc and no url 444 /* If virtual-root isn't specified in cgitrc and no url
439 * parameter is specified on the querystring, lets pretend 445 * parameter is specified on the querystring, lets pretend
440 * that virtualroot equals SCRIPT_NAME and use PATH_INFO as 446 * that virtualroot equals SCRIPT_NAME and use PATH_INFO as
441 * url. This allows cgit to work with virtual urls without 447 * url. This allows cgit to work with virtual urls without
442 * the need for rewriterules in the webserver (as long as 448 * the need for rewriterules in the webserver (as long as
443 * PATH_INFO is included in the cache lookup key). 449 * PATH_INFO is included in the cache lookup key).
444 */ 450 */
445 if (!ctx.cfg.virtual_root && !ctx.qry.url) { 451 if (!ctx.cfg.virtual_root && !ctx.qry.url) {
446 ctx.cfg.virtual_root = ctx.cfg.script_name; 452 ctx.cfg.virtual_root = ctx.cfg.script_name;
447 path = getenv("PATH_INFO"); 453 path = getenv("PATH_INFO");
448 if (path) { 454 if (path) {
449 if (path[0] == '/') 455 if (path[0] == '/')
450 path++; 456 path++;
451 ctx.qry.url = xstrdup(path); 457 ctx.qry.url = xstrdup(path);
452 if (ctx.qry.raw) { 458 if (ctx.qry.raw) {
453 qry = ctx.qry.raw; 459 qry = ctx.qry.raw;
454 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 460 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
455 free(qry); 461 free(qry);
456 } else 462 } else
457 ctx.qry.raw = ctx.qry.url; 463 ctx.qry.raw = ctx.qry.url;
458 cgit_parse_url(ctx.qry.url); 464 cgit_parse_url(ctx.qry.url);
459 } 465 }
460 } 466 }
461 467
462 ttl = calc_ttl(); 468 ttl = calc_ttl();
463 ctx.page.expires += ttl*60; 469 ctx.page.expires += ttl*60;
464 if (ctx.cfg.nocache) 470 if (ctx.cfg.nocache)
465 ctx.cfg.cache_size = 0; 471 ctx.cfg.cache_size = 0;
466 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 472 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
467 ctx.qry.raw, ttl, process_request, &ctx); 473 ctx.qry.raw, ttl, process_request, &ctx);
468 if (err) 474 if (err)
469 cgit_print_error(fmt("Error processing page: %s (%d)", 475 cgit_print_error(fmt("Error processing page: %s (%d)",
470 strerror(err), err)); 476 strerror(err), err));
471 return err; 477 return err;
472} 478}
diff --git a/cgit.css b/cgit.css
index a37d218..ef30fbf 100644
--- a/cgit.css
+++ b/cgit.css
@@ -75,384 +75,461 @@ table.tabs td {
75 padding: 0px 1em; 75 padding: 0px 1em;
76 vertical-align: bottom; 76 vertical-align: bottom;
77} 77}
78 78
79table.tabs td a { 79table.tabs td a {
80 padding: 2px 0.75em; 80 padding: 2px 0.75em;
81 color: #777; 81 color: #777;
82 font-size: 110%; 82 font-size: 110%;
83} 83}
84 84
85table.tabs td a.active { 85table.tabs td a.active {
86 color: #000; 86 color: #000;
87 background-color: #ccc; 87 background-color: #ccc;
88} 88}
89 89
90table.tabs td.form { 90table.tabs td.form {
91 text-align: right; 91 text-align: right;
92} 92}
93 93
94table.tabs td.form form { 94table.tabs td.form form {
95 padding-bottom: 2px; 95 padding-bottom: 2px;
96 font-size: 90%; 96 font-size: 90%;
97 white-space: nowrap; 97 white-space: nowrap;
98} 98}
99 99
100table.tabs td.form input, 100table.tabs td.form input,
101table.tabs td.form select { 101table.tabs td.form select {
102 font-size: 90%; 102 font-size: 90%;
103} 103}
104 104
105div.content { 105div.content {
106 margin: 0px; 106 margin: 0px;
107 padding: 2em; 107 padding: 2em;
108 border-top: solid 3px #ccc; 108 border-top: solid 3px #ccc;
109 border-bottom: solid 3px #ccc; 109 border-bottom: solid 3px #ccc;
110} 110}
111 111
112 112
113table.list { 113table.list {
114 width: 100%; 114 width: 100%;
115 border: none; 115 border: none;
116 border-collapse: collapse; 116 border-collapse: collapse;
117} 117}
118 118
119table.list tr { 119table.list tr {
120 background: white; 120 background: white;
121} 121}
122 122
123table.list tr:hover { 123table.list tr:hover {
124 background: #eee; 124 background: #eee;
125} 125}
126 126
127table.list tr.nohover:hover { 127table.list tr.nohover:hover {
128 background: white; 128 background: white;
129} 129}
130 130
131table.list th { 131table.list th {
132 font-weight: bold; 132 font-weight: bold;
133 /* color: #888; 133 /* color: #888;
134 border-top: dashed 1px #888; 134 border-top: dashed 1px #888;
135 border-bottom: dashed 1px #888; 135 border-bottom: dashed 1px #888;
136 */ 136 */
137 padding: 0.1em 0.5em 0.05em 0.5em; 137 padding: 0.1em 0.5em 0.05em 0.5em;
138 vertical-align: baseline; 138 vertical-align: baseline;
139} 139}
140 140
141table.list td { 141table.list td {
142 border: none; 142 border: none;
143 padding: 0.1em 0.5em 0.1em 0.5em; 143 padding: 0.1em 0.5em 0.1em 0.5em;
144} 144}
145 145
146table.list td a { 146table.list td a {
147 color: black; 147 color: black;
148} 148}
149 149
150table.list td a:hover { 150table.list td a:hover {
151 color: #00f; 151 color: #00f;
152} 152}
153 153
154img { 154img {
155 border: none; 155 border: none;
156} 156}
157 157
158input#switch-btn { 158input#switch-btn {
159 margin: 2px 0px 0px 0px; 159 margin: 2px 0px 0px 0px;
160} 160}
161 161
162td#sidebar input.txt { 162td#sidebar input.txt {
163 width: 100%; 163 width: 100%;
164 margin: 2px 0px 0px 0px; 164 margin: 2px 0px 0px 0px;
165} 165}
166 166
167table#grid { 167table#grid {
168 margin: 0px; 168 margin: 0px;
169} 169}
170 170
171td#content { 171td#content {
172 vertical-align: top; 172 vertical-align: top;
173 padding: 1em 2em 1em 1em; 173 padding: 1em 2em 1em 1em;
174 border: none; 174 border: none;
175} 175}
176 176
177div#summary { 177div#summary {
178 vertical-align: top; 178 vertical-align: top;
179 margin-bottom: 1em; 179 margin-bottom: 1em;
180} 180}
181 181
182table#downloads { 182table#downloads {
183 float: right; 183 float: right;
184 border-collapse: collapse; 184 border-collapse: collapse;
185 border: solid 1px #777; 185 border: solid 1px #777;
186 margin-left: 0.5em; 186 margin-left: 0.5em;
187 margin-bottom: 0.5em; 187 margin-bottom: 0.5em;
188} 188}
189 189
190table#downloads th { 190table#downloads th {
191 background-color: #ccc; 191 background-color: #ccc;
192} 192}
193 193
194div#blob { 194div#blob {
195 border: solid 1px black; 195 border: solid 1px black;
196} 196}
197 197
198div.error { 198div.error {
199 color: red; 199 color: red;
200 font-weight: bold; 200 font-weight: bold;
201 margin: 1em 2em; 201 margin: 1em 2em;
202} 202}
203 203
204a.ls-blob, a.ls-dir, a.ls-mod { 204a.ls-blob, a.ls-dir, a.ls-mod {
205 font-family: monospace; 205 font-family: monospace;
206} 206}
207 207
208td.ls-size { 208td.ls-size {
209 text-align: right; 209 text-align: right;
210 font-family: monospace; 210 font-family: monospace;
211 width: 10em; 211 width: 10em;
212} 212}
213 213
214td.ls-mode { 214td.ls-mode {
215 font-family: monospace; 215 font-family: monospace;
216 width: 10em; 216 width: 10em;
217} 217}
218 218
219table.blob { 219table.blob {
220 margin-top: 0.5em; 220 margin-top: 0.5em;
221 border-top: solid 1px black; 221 border-top: solid 1px black;
222} 222}
223 223
224table.blob td.no { 224table.blob td.no {
225 border-right: solid 1px black; 225 border-right: solid 1px black;
226 color: black; 226 color: black;
227 background-color: #eee; 227 background-color: #eee;
228 text-align: right; 228 text-align: right;
229} 229}
230 230
231table.blob td.no a { 231table.blob td.no a {
232 color: black; 232 color: black;
233} 233}
234 234
235table.blob td.no a:hover { 235table.blob td.no a:hover {
236 color: black; 236 color: black;
237 text-decoration: none; 237 text-decoration: none;
238} 238}
239 239
240table.blob td.txt { 240table.blob td.txt {
241 white-space: pre; 241 white-space: pre;
242 font-family: monospace; 242 font-family: monospace;
243 padding-left: 0.5em; 243 padding-left: 0.5em;
244} 244}
245 245
246table.nowrap td { 246table.nowrap td {
247 white-space: nowrap; 247 white-space: nowrap;
248} 248}
249 249
250table.commit-info { 250table.commit-info {
251 border-collapse: collapse; 251 border-collapse: collapse;
252 margin-top: 1.5em; 252 margin-top: 1.5em;
253} 253}
254 254
255table.commit-info th { 255table.commit-info th {
256 text-align: left; 256 text-align: left;
257 font-weight: normal; 257 font-weight: normal;
258 padding: 0.1em 1em 0.1em 0.1em; 258 padding: 0.1em 1em 0.1em 0.1em;
259 vertical-align: top; 259 vertical-align: top;
260} 260}
261 261
262table.commit-info td { 262table.commit-info td {
263 font-weight: normal; 263 font-weight: normal;
264 padding: 0.1em 1em 0.1em 0.1em; 264 padding: 0.1em 1em 0.1em 0.1em;
265} 265}
266 266
267div.commit-subject { 267div.commit-subject {
268 font-weight: bold; 268 font-weight: bold;
269 font-size: 125%; 269 font-size: 125%;
270 margin: 1.5em 0em 0.5em 0em; 270 margin: 1.5em 0em 0.5em 0em;
271 padding: 0em; 271 padding: 0em;
272} 272}
273 273
274div.commit-msg { 274div.commit-msg {
275 white-space: pre; 275 white-space: pre;
276 font-family: monospace; 276 font-family: monospace;
277} 277}
278 278
279div.diffstat-header { 279div.diffstat-header {
280 font-weight: bold; 280 font-weight: bold;
281 padding-top: 1.5em; 281 padding-top: 1.5em;
282} 282}
283 283
284table.diffstat { 284table.diffstat {
285 border-collapse: collapse; 285 border-collapse: collapse;
286 border: solid 1px #aaa; 286 border: solid 1px #aaa;
287 background-color: #eee; 287 background-color: #eee;
288} 288}
289 289
290table.diffstat th { 290table.diffstat th {
291 font-weight: normal; 291 font-weight: normal;
292 text-align: left; 292 text-align: left;
293 text-decoration: underline; 293 text-decoration: underline;
294 padding: 0.1em 1em 0.1em 0.1em; 294 padding: 0.1em 1em 0.1em 0.1em;
295 font-size: 100%; 295 font-size: 100%;
296} 296}
297 297
298table.diffstat td { 298table.diffstat td {
299 padding: 0.2em 0.2em 0.1em 0.1em; 299 padding: 0.2em 0.2em 0.1em 0.1em;
300 font-size: 100%; 300 font-size: 100%;
301 border: none; 301 border: none;
302} 302}
303 303
304table.diffstat td.mode { 304table.diffstat td.mode {
305 white-space: nowrap; 305 white-space: nowrap;
306} 306}
307 307
308table.diffstat td span.modechange { 308table.diffstat td span.modechange {
309 padding-left: 1em; 309 padding-left: 1em;
310 color: red; 310 color: red;
311} 311}
312 312
313table.diffstat td.add a { 313table.diffstat td.add a {
314 color: green; 314 color: green;
315} 315}
316 316
317table.diffstat td.del a { 317table.diffstat td.del a {
318 color: red; 318 color: red;
319} 319}
320 320
321table.diffstat td.upd a { 321table.diffstat td.upd a {
322 color: blue; 322 color: blue;
323} 323}
324 324
325table.diffstat td.graph { 325table.diffstat td.graph {
326 width: 500px; 326 width: 500px;
327 vertical-align: middle; 327 vertical-align: middle;
328} 328}
329 329
330table.diffstat td.graph table { 330table.diffstat td.graph table {
331 border: none; 331 border: none;
332} 332}
333 333
334table.diffstat td.graph td { 334table.diffstat td.graph td {
335 padding: 0px; 335 padding: 0px;
336 border: 0px; 336 border: 0px;
337 height: 7pt; 337 height: 7pt;
338} 338}
339 339
340table.diffstat td.graph td.add { 340table.diffstat td.graph td.add {
341 background-color: #5c5; 341 background-color: #5c5;
342} 342}
343 343
344table.diffstat td.graph td.rem { 344table.diffstat td.graph td.rem {
345 background-color: #c55; 345 background-color: #c55;
346} 346}
347 347
348div.diffstat-summary { 348div.diffstat-summary {
349 color: #888; 349 color: #888;
350 padding-top: 0.5em; 350 padding-top: 0.5em;
351} 351}
352 352
353table.diff { 353table.diff {
354 width: 100%; 354 width: 100%;
355} 355}
356 356
357table.diff td { 357table.diff td {
358 font-family: monospace; 358 font-family: monospace;
359 white-space: pre; 359 white-space: pre;
360} 360}
361 361
362table.diff td div.head { 362table.diff td div.head {
363 font-weight: bold; 363 font-weight: bold;
364 margin-top: 1em; 364 margin-top: 1em;
365 color: black; 365 color: black;
366} 366}
367 367
368table.diff td div.hunk { 368table.diff td div.hunk {
369 color: #009; 369 color: #009;
370} 370}
371 371
372table.diff td div.add { 372table.diff td div.add {
373 color: green; 373 color: green;
374} 374}
375 375
376table.diff td div.del { 376table.diff td div.del {
377 color: red; 377 color: red;
378} 378}
379 379
380.sha1 { 380.sha1 {
381 font-family: monospace; 381 font-family: monospace;
382 font-size: 90%; 382 font-size: 90%;
383} 383}
384 384
385.left { 385.left {
386 text-align: left; 386 text-align: left;
387} 387}
388 388
389.right { 389.right {
390 text-align: right; 390 text-align: right;
391} 391}
392 392
393table.list td.repogroup { 393table.list td.repogroup {
394 font-style: italic; 394 font-style: italic;
395 color: #888; 395 color: #888;
396} 396}
397 397
398a.button { 398a.button {
399 font-size: 80%; 399 font-size: 80%;
400 padding: 0em 0.5em; 400 padding: 0em 0.5em;
401} 401}
402 402
403a.primary { 403a.primary {
404 font-size: 100%; 404 font-size: 100%;
405} 405}
406 406
407a.secondary { 407a.secondary {
408 font-size: 90%; 408 font-size: 90%;
409} 409}
410 410
411td.toplevel-repo { 411td.toplevel-repo {
412 412
413} 413}
414 414
415table.list td.sublevel-repo { 415table.list td.sublevel-repo {
416 padding-left: 1.5em; 416 padding-left: 1.5em;
417} 417}
418 418
419div.pager { 419div.pager {
420 text-align: center; 420 text-align: center;
421 margin: 1em 0em 0em 0em; 421 margin: 1em 0em 0em 0em;
422} 422}
423 423
424div.pager a { 424div.pager a {
425 color: #777; 425 color: #777;
426 margin: 0em 0.5em; 426 margin: 0em 0.5em;
427} 427}
428 428
429span.age-mins { 429span.age-mins {
430 font-weight: bold; 430 font-weight: bold;
431 color: #080; 431 color: #080;
432} 432}
433 433
434span.age-hours { 434span.age-hours {
435 color: #080; 435 color: #080;
436} 436}
437 437
438span.age-days { 438span.age-days {
439 color: #040; 439 color: #040;
440} 440}
441 441
442span.age-weeks { 442span.age-weeks {
443 color: #444; 443 color: #444;
444} 444}
445 445
446span.age-months { 446span.age-months {
447 color: #888; 447 color: #888;
448} 448}
449 449
450span.age-years { 450span.age-years {
451 color: #bbb; 451 color: #bbb;
452} 452}
453div.footer { 453div.footer {
454 margin-top: 0.5em; 454 margin-top: 0.5em;
455 text-align: center; 455 text-align: center;
456 font-size: 80%; 456 font-size: 80%;
457 color: #ccc; 457 color: #ccc;
458} 458}
459table.stats {
460 border: solid 1px black;
461 border-collapse: collapse;
462}
463
464table.stats th {
465 text-align: left;
466 padding: 1px 0.5em;
467 background-color: #eee;
468 border: solid 1px black;
469}
470
471table.stats td {
472 text-align: right;
473 padding: 1px 0.5em;
474 border: solid 1px black;
475}
476
477table.stats td.total {
478 font-weight: bold;
479 text-align: left;
480}
481
482table.stats td.sum {
483 color: #c00;
484 font-weight: bold;
485 /*background-color: #eee; */
486}
487
488table.stats td.left {
489 text-align: left;
490}
491
492table.vgraph {
493 border-collapse: separate;
494 border: solid 1px black;
495 height: 200px;
496}
497
498table.vgraph th {
499 background-color: #eee;
500 font-weight: bold;
501 border: solid 1px white;
502 padding: 1px 0.5em;
503}
504
505table.vgraph td {
506 vertical-align: bottom;
507 padding: 0px 10px;
508}
509
510table.vgraph div.bar {
511 background-color: #eee;
512}
513
514table.hgraph {
515 border: solid 1px black;
516 width: 800px;
517}
518
519table.hgraph th {
520 background-color: #eee;
521 font-weight: bold;
522 border: solid 1px black;
523 padding: 1px 0.5em;
524}
525
526table.hgraph td {
527 vertical-align: center;
528 padding: 2px 2px;
529}
530
531table.hgraph div.bar {
532 background-color: #eee;
533 height: 1em;
534}
535
diff --git a/cgit.h b/cgit.h
index 91db98a..85045c4 100644
--- a/cgit.h
+++ b/cgit.h
@@ -1,243 +1,246 @@
1#ifndef CGIT_H 1#ifndef CGIT_H
2#define CGIT_H 2#define CGIT_H
3 3
4 4
5#include <git-compat-util.h> 5#include <git-compat-util.h>
6#include <cache.h> 6#include <cache.h>
7#include <grep.h> 7#include <grep.h>
8#include <object.h> 8#include <object.h>
9#include <tree.h> 9#include <tree.h>
10#include <commit.h> 10#include <commit.h>
11#include <tag.h> 11#include <tag.h>
12#include <diff.h> 12#include <diff.h>
13#include <diffcore.h> 13#include <diffcore.h>
14#include <refs.h> 14#include <refs.h>
15#include <revision.h> 15#include <revision.h>
16#include <log-tree.h> 16#include <log-tree.h>
17#include <archive.h> 17#include <archive.h>
18#include <xdiff/xdiff.h> 18#include <xdiff/xdiff.h>
19#include <utf8.h> 19#include <utf8.h>
20 20
21 21
22/* 22/*
23 * Dateformats used on misc. pages 23 * Dateformats used on misc. pages
24 */ 24 */
25#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)" 25#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)"
26#define FMT_SHORTDATE "%Y-%m-%d" 26#define FMT_SHORTDATE "%Y-%m-%d"
27#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ" 27#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ"
28 28
29 29
30/* 30/*
31 * Limits used for relative dates 31 * Limits used for relative dates
32 */ 32 */
33#define TM_MIN 60 33#define TM_MIN 60
34#define TM_HOUR (TM_MIN * 60) 34#define TM_HOUR (TM_MIN * 60)
35#define TM_DAY (TM_HOUR * 24) 35#define TM_DAY (TM_HOUR * 24)
36#define TM_WEEK (TM_DAY * 7) 36#define TM_WEEK (TM_DAY * 7)
37#define TM_YEAR (TM_DAY * 365) 37#define TM_YEAR (TM_DAY * 365)
38#define TM_MONTH (TM_YEAR / 12.0) 38#define TM_MONTH (TM_YEAR / 12.0)
39 39
40 40
41/* 41/*
42 * Default encoding 42 * Default encoding
43 */ 43 */
44#define PAGE_ENCODING "UTF-8" 44#define PAGE_ENCODING "UTF-8"
45 45
46typedef void (*configfn)(const char *name, const char *value); 46typedef void (*configfn)(const char *name, const char *value);
47typedef void (*filepair_fn)(struct diff_filepair *pair); 47typedef void (*filepair_fn)(struct diff_filepair *pair);
48typedef void (*linediff_fn)(char *line, int len); 48typedef void (*linediff_fn)(char *line, int len);
49 49
50struct cgit_repo { 50struct cgit_repo {
51 char *url; 51 char *url;
52 char *name; 52 char *name;
53 char *path; 53 char *path;
54 char *desc; 54 char *desc;
55 char *owner; 55 char *owner;
56 char *defbranch; 56 char *defbranch;
57 char *group; 57 char *group;
58 char *module_link; 58 char *module_link;
59 char *readme; 59 char *readme;
60 char *clone_url; 60 char *clone_url;
61 int snapshots; 61 int snapshots;
62 int enable_log_filecount; 62 int enable_log_filecount;
63 int enable_log_linecount; 63 int enable_log_linecount;
64 int enable_stats;
64}; 65};
65 66
66struct cgit_repolist { 67struct cgit_repolist {
67 int length; 68 int length;
68 int count; 69 int count;
69 struct cgit_repo *repos; 70 struct cgit_repo *repos;
70}; 71};
71 72
72struct commitinfo { 73struct commitinfo {
73 struct commit *commit; 74 struct commit *commit;
74 char *author; 75 char *author;
75 char *author_email; 76 char *author_email;
76 unsigned long author_date; 77 unsigned long author_date;
77 char *committer; 78 char *committer;
78 char *committer_email; 79 char *committer_email;
79 unsigned long committer_date; 80 unsigned long committer_date;
80 char *subject; 81 char *subject;
81 char *msg; 82 char *msg;
82 char *msg_encoding; 83 char *msg_encoding;
83}; 84};
84 85
85struct taginfo { 86struct taginfo {
86 char *tagger; 87 char *tagger;
87 char *tagger_email; 88 char *tagger_email;
88 unsigned long tagger_date; 89 unsigned long tagger_date;
89 char *msg; 90 char *msg;
90}; 91};
91 92
92struct refinfo { 93struct refinfo {
93 const char *refname; 94 const char *refname;
94 struct object *object; 95 struct object *object;
95 union { 96 union {
96 struct taginfo *tag; 97 struct taginfo *tag;
97 struct commitinfo *commit; 98 struct commitinfo *commit;
98 }; 99 };
99}; 100};
100 101
101struct reflist { 102struct reflist {
102 struct refinfo **refs; 103 struct refinfo **refs;
103 int alloc; 104 int alloc;
104 int count; 105 int count;
105}; 106};
106 107
107struct cgit_query { 108struct cgit_query {
108 int has_symref; 109 int has_symref;
109 int has_sha1; 110 int has_sha1;
110 char *raw; 111 char *raw;
111 char *repo; 112 char *repo;
112 char *page; 113 char *page;
113 char *search; 114 char *search;
114 char *grep; 115 char *grep;
115 char *head; 116 char *head;
116 char *sha1; 117 char *sha1;
117 char *sha2; 118 char *sha2;
118 char *path; 119 char *path;
119 char *name; 120 char *name;
120 char *mimetype; 121 char *mimetype;
121 char *url; 122 char *url;
123 char *period;
122 int ofs; 124 int ofs;
123 int nohead; 125 int nohead;
124}; 126};
125 127
126struct cgit_config { 128struct cgit_config {
127 char *agefile; 129 char *agefile;
128 char *cache_root; 130 char *cache_root;
129 char *clone_prefix; 131 char *clone_prefix;
130 char *css; 132 char *css;
131 char *favicon; 133 char *favicon;
132 char *footer; 134 char *footer;
133 char *index_header; 135 char *index_header;
134 char *index_info; 136 char *index_info;
135 char *logo; 137 char *logo;
136 char *logo_link; 138 char *logo_link;
137 char *module_link; 139 char *module_link;
138 char *repo_group; 140 char *repo_group;
139 char *robots; 141 char *robots;
140 char *root_title; 142 char *root_title;
141 char *root_desc; 143 char *root_desc;
142 char *root_readme; 144 char *root_readme;
143 char *script_name; 145 char *script_name;
144 char *virtual_root; 146 char *virtual_root;
145 int cache_size; 147 int cache_size;
146 int cache_dynamic_ttl; 148 int cache_dynamic_ttl;
147 int cache_max_create_time; 149 int cache_max_create_time;
148 int cache_repo_ttl; 150 int cache_repo_ttl;
149 int cache_root_ttl; 151 int cache_root_ttl;
150 int cache_static_ttl; 152 int cache_static_ttl;
151 int enable_index_links; 153 int enable_index_links;
152 int enable_log_filecount; 154 int enable_log_filecount;
153 int enable_log_linecount; 155 int enable_log_linecount;
156 int enable_stats;
154 int local_time; 157 int local_time;
155 int max_repo_count; 158 int max_repo_count;
156 int max_commit_count; 159 int max_commit_count;
157 int max_lock_attempts; 160 int max_lock_attempts;
158 int max_msg_len; 161 int max_msg_len;
159 int max_repodesc_len; 162 int max_repodesc_len;
160 int nocache; 163 int nocache;
161 int renamelimit; 164 int renamelimit;
162 int snapshots; 165 int snapshots;
163 int summary_branches; 166 int summary_branches;
164 int summary_log; 167 int summary_log;
165 int summary_tags; 168 int summary_tags;
166}; 169};
167 170
168struct cgit_page { 171struct cgit_page {
169 time_t modified; 172 time_t modified;
170 time_t expires; 173 time_t expires;
171 size_t size; 174 size_t size;
172 char *mimetype; 175 char *mimetype;
173 char *charset; 176 char *charset;
174 char *filename; 177 char *filename;
175 char *title; 178 char *title;
176}; 179};
177 180
178struct cgit_context { 181struct cgit_context {
179 struct cgit_query qry; 182 struct cgit_query qry;
180 struct cgit_config cfg; 183 struct cgit_config cfg;
181 struct cgit_repo *repo; 184 struct cgit_repo *repo;
182 struct cgit_page page; 185 struct cgit_page page;
183}; 186};
184 187
185struct cgit_snapshot_format { 188struct cgit_snapshot_format {
186 const char *suffix; 189 const char *suffix;
187 const char *mimetype; 190 const char *mimetype;
188 write_archive_fn_t write_func; 191 write_archive_fn_t write_func;
189 int bit; 192 int bit;
190}; 193};
191 194
192extern const char *cgit_version; 195extern const char *cgit_version;
193 196
194extern struct cgit_repolist cgit_repolist; 197extern struct cgit_repolist cgit_repolist;
195extern struct cgit_context ctx; 198extern struct cgit_context ctx;
196extern const struct cgit_snapshot_format cgit_snapshot_formats[]; 199extern const struct cgit_snapshot_format cgit_snapshot_formats[];
197 200
198extern struct cgit_repo *cgit_add_repo(const char *url); 201extern struct cgit_repo *cgit_add_repo(const char *url);
199extern struct cgit_repo *cgit_get_repoinfo(const char *url); 202extern struct cgit_repo *cgit_get_repoinfo(const char *url);
200extern void cgit_repo_config_cb(const char *name, const char *value); 203extern void cgit_repo_config_cb(const char *name, const char *value);
201 204
202extern int chk_zero(int result, char *msg); 205extern int chk_zero(int result, char *msg);
203extern int chk_positive(int result, char *msg); 206extern int chk_positive(int result, char *msg);
204extern int chk_non_negative(int result, char *msg); 207extern int chk_non_negative(int result, char *msg);
205 208
206extern char *trim_end(const char *str, char c); 209extern char *trim_end(const char *str, char c);
207extern char *strlpart(char *txt, int maxlen); 210extern char *strlpart(char *txt, int maxlen);
208extern char *strrpart(char *txt, int maxlen); 211extern char *strrpart(char *txt, int maxlen);
209 212
210extern void cgit_add_ref(struct reflist *list, struct refinfo *ref); 213extern void cgit_add_ref(struct reflist *list, struct refinfo *ref);
211extern int cgit_refs_cb(const char *refname, const unsigned char *sha1, 214extern int cgit_refs_cb(const char *refname, const unsigned char *sha1,
212 int flags, void *cb_data); 215 int flags, void *cb_data);
213 216
214extern void *cgit_free_commitinfo(struct commitinfo *info); 217extern void *cgit_free_commitinfo(struct commitinfo *info);
215 218
216extern int cgit_diff_files(const unsigned char *old_sha1, 219extern int cgit_diff_files(const unsigned char *old_sha1,
217 const unsigned char *new_sha1, 220 const unsigned char *new_sha1,
218 linediff_fn fn); 221 linediff_fn fn);
219 222
220extern void cgit_diff_tree(const unsigned char *old_sha1, 223extern void cgit_diff_tree(const unsigned char *old_sha1,
221 const unsigned char *new_sha1, 224 const unsigned char *new_sha1,
222 filepair_fn fn, const char *prefix); 225 filepair_fn fn, const char *prefix);
223 226
224extern void cgit_diff_commit(struct commit *commit, filepair_fn fn); 227extern void cgit_diff_commit(struct commit *commit, filepair_fn fn);
225 228
226extern char *fmt(const char *format,...); 229extern char *fmt(const char *format,...);
227 230
228extern struct commitinfo *cgit_parse_commit(struct commit *commit); 231extern struct commitinfo *cgit_parse_commit(struct commit *commit);
229extern struct taginfo *cgit_parse_tag(struct tag *tag); 232extern struct taginfo *cgit_parse_tag(struct tag *tag);
230extern void cgit_parse_url(const char *url); 233extern void cgit_parse_url(const char *url);
231 234
232extern const char *cgit_repobasename(const char *reponame); 235extern const char *cgit_repobasename(const char *reponame);
233 236
234extern int cgit_parse_snapshots_mask(const char *str); 237extern int cgit_parse_snapshots_mask(const char *str);
235 238
236/* libgit.a either links against or compiles its own implementation of 239/* libgit.a either links against or compiles its own implementation of
237 * strcasestr(), and we'd like to reuse it. Simply re-declaring it 240 * strcasestr(), and we'd like to reuse it. Simply re-declaring it
238 * seems to do the trick. 241 * seems to do the trick.
239 */ 242 */
240extern char *strcasestr(const char *haystack, const char *needle); 243extern char *strcasestr(const char *haystack, const char *needle);
241 244
242 245
243#endif /* CGIT_H */ 246#endif /* CGIT_H */
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 7887b02..60d3ea4 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -1,367 +1,375 @@
1CGITRC 1CGITRC
2====== 2======
3 3
4 4
5NAME 5NAME
6---- 6----
7 cgitrc - runtime configuration for cgit 7 cgitrc - runtime configuration for cgit
8 8
9 9
10DESCRIPTION 10DESCRIPTION
11----------- 11-----------
12Cgitrc contains all runtime settings for cgit, including the list of git 12Cgitrc contains all runtime settings for cgit, including the list of git
13repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank 13repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank
14lines, and lines starting with '#', are ignored. 14lines, and lines starting with '#', are ignored.
15 15
16 16
17GLOBAL SETTINGS 17GLOBAL SETTINGS
18--------------- 18---------------
19agefile 19agefile
20 Specifies a path, relative to each repository path, which can be used 20 Specifies a path, relative to each repository path, which can be used
21 to specify the date and time of the youngest commit in the repository. 21 to specify the date and time of the youngest commit in the repository.
22 The first line in the file is used as input to the "parse_date" 22 The first line in the file is used as input to the "parse_date"
23 function in libgit. Recommended timestamp-format is "yyyy-mm-dd 23 function in libgit. Recommended timestamp-format is "yyyy-mm-dd
24 hh:mm:ss". Default value: "info/web/last-modified". 24 hh:mm:ss". Default value: "info/web/last-modified".
25 25
26cache-root 26cache-root
27 Path used to store the cgit cache entries. Default value: 27 Path used to store the cgit cache entries. Default value:
28 "/var/cache/cgit". 28 "/var/cache/cgit".
29 29
30cache-dynamic-ttl 30cache-dynamic-ttl
31 Number which specifies the time-to-live, in minutes, for the cached 31 Number which specifies the time-to-live, in minutes, for the cached
32 version of repository pages accessed without a fixed SHA1. Default 32 version of repository pages accessed without a fixed SHA1. Default
33 value: "5". 33 value: "5".
34 34
35cache-repo-ttl 35cache-repo-ttl
36 Number which specifies the time-to-live, in minutes, for the cached 36 Number which specifies the time-to-live, in minutes, for the cached
37 version of the repository summary page. Default value: "5". 37 version of the repository summary page. Default value: "5".
38 38
39cache-root-ttl 39cache-root-ttl
40 Number which specifies the time-to-live, in minutes, for the cached 40 Number which specifies the time-to-live, in minutes, for the cached
41 version of the repository index page. Default value: "5". 41 version of the repository index page. Default value: "5".
42 42
43cache-size 43cache-size
44 The maximum number of entries in the cgit cache. Default value: "0" 44 The maximum number of entries in the cgit cache. Default value: "0"
45 (i.e. caching is disabled). 45 (i.e. caching is disabled).
46 46
47cache-static-ttl 47cache-static-ttl
48 Number which specifies the time-to-live, in minutes, for the cached 48 Number which specifies the time-to-live, in minutes, for the cached
49 version of repository pages accessed with a fixed SHA1. Default value: 49 version of repository pages accessed with a fixed SHA1. Default value:
50 "5". 50 "5".
51 51
52clone-prefix 52clone-prefix
53 Space-separated list of common prefixes which, when combined with a 53 Space-separated list of common prefixes which, when combined with a
54 repository url, generates valid clone urls for the repository. This 54 repository url, generates valid clone urls for the repository. This
55 setting is only used if `repo.clone-url` is unspecified. Default value: 55 setting is only used if `repo.clone-url` is unspecified. Default value:
56 none. 56 none.
57 57
58css 58css
59 Url which specifies the css document to include in all cgit pages. 59 Url which specifies the css document to include in all cgit pages.
60 Default value: "/cgit.css". 60 Default value: "/cgit.css".
61 61
62enable-index-links 62enable-index-links
63 Flag which, when set to "1", will make cgit generate extra links for 63 Flag which, when set to "1", will make cgit generate extra links for
64 each repo in the repository index (specifically, to the "summary", 64 each repo in the repository index (specifically, to the "summary",
65 "commit" and "tree" pages). Default value: "0". 65 "commit" and "tree" pages). Default value: "0".
66 66
67enable-log-filecount 67enable-log-filecount
68 Flag which, when set to "1", will make cgit print the number of 68 Flag which, when set to "1", will make cgit print the number of
69 modified files for each commit on the repository log page. Default 69 modified files for each commit on the repository log page. Default
70 value: "0". 70 value: "0".
71 71
72enable-log-linecount 72enable-log-linecount
73 Flag which, when set to "1", will make cgit print the number of added 73 Flag which, when set to "1", will make cgit print the number of added
74 and removed lines for each commit on the repository log page. Default 74 and removed lines for each commit on the repository log page. Default
75 value: "0". 75 value: "0".
76 76
77enable-stats
78 Globally enable/disable statistics for each repository. Default
79 value: "0".
80
77favicon 81favicon
78 Url used as link to a shortcut icon for cgit. If specified, it is 82 Url used as link to a shortcut icon for cgit. If specified, it is
79 suggested to use the value "/favicon.ico" since certain browsers will 83 suggested to use the value "/favicon.ico" since certain browsers will
80 ignore other values. Default value: none. 84 ignore other values. Default value: none.
81 85
82footer 86footer
83 The content of the file specified with this option will be included 87 The content of the file specified with this option will be included
84 verbatim at the bottom of all pages (i.e. it replaces the standard 88 verbatim at the bottom of all pages (i.e. it replaces the standard
85 "generated by..." message. Default value: none. 89 "generated by..." message. Default value: none.
86 90
87include 91include
88 Name of a configfile to include before the rest of the current config- 92 Name of a configfile to include before the rest of the current config-
89 file is parsed. Default value: none. 93 file is parsed. Default value: none.
90 94
91index-header 95index-header
92 The content of the file specified with this option will be included 96 The content of the file specified with this option will be included
93 verbatim above the repository index. This setting is deprecated, and 97 verbatim above the repository index. This setting is deprecated, and
94 will not be supported by cgit-1.0 (use root-readme instead). Default 98 will not be supported by cgit-1.0 (use root-readme instead). Default
95 value: none. 99 value: none.
96 100
97index-info 101index-info
98 The content of the file specified with this option will be included 102 The content of the file specified with this option will be included
99 verbatim below the heading on the repository index page. This setting 103 verbatim below the heading on the repository index page. This setting
100 is deprecated, and will not be supported by cgit-1.0 (use root-desc 104 is deprecated, and will not be supported by cgit-1.0 (use root-desc
101 instead). Default value: none. 105 instead). Default value: none.
102 106
103local-time 107local-time
104 Flag which, if set to "1", makes cgit print commit and tag times in the 108 Flag which, if set to "1", makes cgit print commit and tag times in the
105 servers timezone. Default value: "0". 109 servers timezone. Default value: "0".
106 110
107logo 111logo
108 Url which specifies the source of an image which will be used as a logo 112 Url which specifies the source of an image which will be used as a logo
109 on all cgit pages. 113 on all cgit pages.
110 114
111logo-link 115logo-link
112 Url loaded when clicking on the cgit logo image. If unspecified the 116 Url loaded when clicking on the cgit logo image. If unspecified the
113 calculated url of the repository index page will be used. Default 117 calculated url of the repository index page will be used. Default
114 value: none. 118 value: none.
115 119
116max-commit-count 120max-commit-count
117 Specifies the number of entries to list per page in "log" view. Default 121 Specifies the number of entries to list per page in "log" view. Default
118 value: "50". 122 value: "50".
119 123
120max-message-length 124max-message-length
121 Specifies the maximum number of commit message characters to display in 125 Specifies the maximum number of commit message characters to display in
122 "log" view. Default value: "80". 126 "log" view. Default value: "80".
123 127
124max-repo-count 128max-repo-count
125 Specifies the number of entries to list per page on therepository 129 Specifies the number of entries to list per page on therepository
126 index page. Default value: "50". 130 index page. Default value: "50".
127 131
128max-repodesc-length 132max-repodesc-length
129 Specifies the maximum number of repo description characters to display 133 Specifies the maximum number of repo description characters to display
130 on the repository index page. Default value: "80". 134 on the repository index page. Default value: "80".
131 135
132module-link 136module-link
133 Text which will be used as the formatstring for a hyperlink when a 137 Text which will be used as the formatstring for a hyperlink when a
134 submodule is printed in a directory listing. The arguments for the 138 submodule is printed in a directory listing. The arguments for the
135 formatstring are the path and SHA1 of the submodule commit. Default 139 formatstring are the path and SHA1 of the submodule commit. Default
136 value: "./?repo=%s&page=commit&id=%s" 140 value: "./?repo=%s&page=commit&id=%s"
137 141
138nocache 142nocache
139 If set to the value "1" caching will be disabled. This settings is 143 If set to the value "1" caching will be disabled. This settings is
140 deprecated, and will not be honored starting with cgit-1.0. Default 144 deprecated, and will not be honored starting with cgit-1.0. Default
141 value: "0". 145 value: "0".
142 146
143renamelimit 147renamelimit
144 Maximum number of files to consider when detecting renames. The value 148 Maximum number of files to consider when detecting renames. The value
145 "-1" uses the compiletime value in git (for further info, look at 149 "-1" uses the compiletime value in git (for further info, look at
146 `man git-diff`). Default value: "-1". 150 `man git-diff`). Default value: "-1".
147 151
148repo.group 152repo.group
149 A value for the current repository group, which all repositories 153 A value for the current repository group, which all repositories
150 specified after this setting will inherit. Default value: none. 154 specified after this setting will inherit. Default value: none.
151 155
152robots 156robots
153 Text used as content for the "robots" meta-tag. Default value: 157 Text used as content for the "robots" meta-tag. Default value:
154 "index, nofollow". 158 "index, nofollow".
155 159
156root-desc 160root-desc
157 Text printed below the heading on the repository index page. Default 161 Text printed below the heading on the repository index page. Default
158 value: "a fast webinterface for the git dscm". 162 value: "a fast webinterface for the git dscm".
159 163
160root-readme: 164root-readme:
161 The content of the file specified with this option will be included 165 The content of the file specified with this option will be included
162 verbatim below the "about" link on the repository index page. Default 166 verbatim below the "about" link on the repository index page. Default
163 value: none. 167 value: none.
164 168
165root-title 169root-title
166 Text printed as heading on the repository index page. Default value: 170 Text printed as heading on the repository index page. Default value:
167 "Git Repository Browser". 171 "Git Repository Browser".
168 172
169snapshots 173snapshots
170 Text which specifies the default (and allowed) set of snapshot formats 174 Text which specifies the default (and allowed) set of snapshot formats
171 supported by cgit. The value is a space-separated list of zero or more 175 supported by cgit. The value is a space-separated list of zero or more
172 of the following values: 176 of the following values:
173 "tar" uncompressed tar-file 177 "tar" uncompressed tar-file
174 "tar.gz"gzip-compressed tar-file 178 "tar.gz"gzip-compressed tar-file
175 "tar.bz2"bzip-compressed tar-file 179 "tar.bz2"bzip-compressed tar-file
176 "zip" zip-file 180 "zip" zip-file
177 Default value: none. 181 Default value: none.
178 182
179summary-branches 183summary-branches
180 Specifies the number of branches to display in the repository "summary" 184 Specifies the number of branches to display in the repository "summary"
181 view. Default value: "10". 185 view. Default value: "10".
182 186
183summary-log 187summary-log
184 Specifies the number of log entries to display in the repository 188 Specifies the number of log entries to display in the repository
185 "summary" view. Default value: "10". 189 "summary" view. Default value: "10".
186 190
187summary-tags 191summary-tags
188 Specifies the number of tags to display in the repository "summary" 192 Specifies the number of tags to display in the repository "summary"
189 view. Default value: "10". 193 view. Default value: "10".
190 194
191virtual-root 195virtual-root
192 Url which, if specified, will be used as root for all cgit links. It 196 Url which, if specified, will be used as root for all cgit links. It
193 will also cause cgit to generate 'virtual urls', i.e. urls like 197 will also cause cgit to generate 'virtual urls', i.e. urls like
194 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default 198 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default
195 value: none. 199 value: none.
196 NOTE: cgit has recently learned how to use PATH_INFO to achieve the 200 NOTE: cgit has recently learned how to use PATH_INFO to achieve the
197 same kind of virtual urls, so this option will probably be deprecated. 201 same kind of virtual urls, so this option will probably be deprecated.
198 202
199REPOSITORY SETTINGS 203REPOSITORY SETTINGS
200------------------- 204-------------------
201repo.clone-url 205repo.clone-url
202 A list of space-separated urls which can be used to clone this repo. 206 A list of space-separated urls which can be used to clone this repo.
203 Default value: none. 207 Default value: none.
204 208
205repo.defbranch 209repo.defbranch
206 The name of the default branch for this repository. If no such branch 210 The name of the default branch for this repository. If no such branch
207 exists in the repository, the first branch name (when sorted) is used 211 exists in the repository, the first branch name (when sorted) is used
208 as default instead. Default value: "master". 212 as default instead. Default value: "master".
209 213
210repo.desc 214repo.desc
211 The value to show as repository description. Default value: none. 215 The value to show as repository description. Default value: none.
212 216
213repo.enable-log-filecount 217repo.enable-log-filecount
214 A flag which can be used to disable the global setting 218 A flag which can be used to disable the global setting
215 `enable-log-filecount'. Default value: none. 219 `enable-log-filecount'. Default value: none.
216 220
217repo.enable-log-linecount 221repo.enable-log-linecount
218 A flag which can be used to disable the global setting 222 A flag which can be used to disable the global setting
219 `enable-log-linecount'. Default value: none. 223 `enable-log-linecount'. Default value: none.
220 224
225repo.enable-stats
226 A flag which can be used to disable the global setting
227 `enable-stats'. Default value: none.
228
221repo.name 229repo.name
222 The value to show as repository name. Default value: <repo.url>. 230 The value to show as repository name. Default value: <repo.url>.
223 231
224repo.owner 232repo.owner
225 A value used to identify the owner of the repository. Default value: 233 A value used to identify the owner of the repository. Default value:
226 none. 234 none.
227 235
228repo.path 236repo.path
229 An absolute path to the repository directory. For non-bare repositories 237 An absolute path to the repository directory. For non-bare repositories
230 this is the .git-directory. Default value: none. 238 this is the .git-directory. Default value: none.
231 239
232repo.readme 240repo.readme
233 A path (relative to <repo.path>) which specifies a file to include 241 A path (relative to <repo.path>) which specifies a file to include
234 verbatim as the "About" page for this repo. Default value: none. 242 verbatim as the "About" page for this repo. Default value: none.
235 243
236repo.snapshots 244repo.snapshots
237 A mask of allowed snapshot-formats for this repo, restricted by the 245 A mask of allowed snapshot-formats for this repo, restricted by the
238 "snapshots" global setting. Default value: <snapshots>. 246 "snapshots" global setting. Default value: <snapshots>.
239 247
240repo.url 248repo.url
241 The relative url used to access the repository. This must be the first 249 The relative url used to access the repository. This must be the first
242 setting specified for each repo. Default value: none. 250 setting specified for each repo. Default value: none.
243 251
244 252
245EXAMPLE CGITRC FILE 253EXAMPLE CGITRC FILE
246------------------- 254-------------------
247 255
248# Enable caching of up to 1000 output entriess 256# Enable caching of up to 1000 output entriess
249cache-size=1000 257cache-size=1000
250 258
251 259
252# Specify some default clone prefixes 260# Specify some default clone prefixes
253clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git 261clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git
254 262
255# Specify the css url 263# Specify the css url
256css=/css/cgit.css 264css=/css/cgit.css
257 265
258 266
259# Show extra links for each repository on the index page 267# Show extra links for each repository on the index page
260enable-index-links=1 268enable-index-links=1
261 269
262 270
263# Show number of affected files per commit on the log pages 271# Show number of affected files per commit on the log pages
264enable-log-filecount=1 272enable-log-filecount=1
265 273
266 274
267# Show number of added/removed lines per commit on the log pages 275# Show number of added/removed lines per commit on the log pages
268enable-log-linecount=1 276enable-log-linecount=1
269 277
270 278
271# Add a cgit favicon 279# Add a cgit favicon
272favicon=/favicon.ico 280favicon=/favicon.ico
273 281
274 282
275# Use a custom logo 283# Use a custom logo
276logo=/img/mylogo.png 284logo=/img/mylogo.png
277 285
278 286
279# Set the title and heading of the repository index page 287# Set the title and heading of the repository index page
280root-title=foobar.com git repositories 288root-title=foobar.com git repositories
281 289
282 290
283# Set a subheading for the repository index page 291# Set a subheading for the repository index page
284root-desc=tracking the foobar development 292root-desc=tracking the foobar development
285 293
286 294
287# Include some more info about foobar.com on the index page 295# Include some more info about foobar.com on the index page
288root-readme=/var/www/htdocs/about.html 296root-readme=/var/www/htdocs/about.html
289 297
290 298
291# Allow download of tar.gz, tar.bz and zip-files 299# Allow download of tar.gz, tar.bz and zip-files
292snapshots=tar.gz tar.bz zip 300snapshots=tar.gz tar.bz zip
293 301
294 302
295## 303##
296## List of repositories. 304## List of repositories.
297## PS: Any repositories listed when repo.group is unset will not be 305## PS: Any repositories listed when repo.group is unset will not be
298## displayed under a group heading 306## displayed under a group heading
299## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos') 307## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos')
300## and included like this: 308## and included like this:
301## include=/etc/cgitrepos 309## include=/etc/cgitrepos
302## 310##
303 311
304 312
305repo.url=foo 313repo.url=foo
306repo.path=/pub/git/foo.git 314repo.path=/pub/git/foo.git
307repo.desc=the master foo repository 315repo.desc=the master foo repository
308repo.owner=fooman@foobar.com 316repo.owner=fooman@foobar.com
309repo.readme=info/web/about.html 317repo.readme=info/web/about.html
310 318
311 319
312repo.url=bar 320repo.url=bar
313repo.path=/pub/git/bar.git 321repo.path=/pub/git/bar.git
314repo.desc=the bars for your foo 322repo.desc=the bars for your foo
315repo.owner=barman@foobar.com 323repo.owner=barman@foobar.com
316repo.readme=info/web/about.html 324repo.readme=info/web/about.html
317 325
318 326
319# The next repositories will be displayed under the 'extras' heading 327# The next repositories will be displayed under the 'extras' heading
320repo.group=extras 328repo.group=extras
321 329
322 330
323repo.url=baz 331repo.url=baz
324repo.path=/pub/git/baz.git 332repo.path=/pub/git/baz.git
325repo.desc=a set of extensions for bar users 333repo.desc=a set of extensions for bar users
326 334
327repo.url=wiz 335repo.url=wiz
328repo.path=/pub/git/wiz.git 336repo.path=/pub/git/wiz.git
329repo.desc=the wizard of foo 337repo.desc=the wizard of foo
330 338
331 339
332# Add some mirrored repositories 340# Add some mirrored repositories
333repo.group=mirrors 341repo.group=mirrors
334 342
335 343
336repo.url=git 344repo.url=git
337repo.path=/pub/git/git.git 345repo.path=/pub/git/git.git
338repo.desc=the dscm 346repo.desc=the dscm
339 347
340 348
341repo.url=linux 349repo.url=linux
342repo.path=/pub/git/linux.git 350repo.path=/pub/git/linux.git
343repo.desc=the kernel 351repo.desc=the kernel
344 352
345# Disable adhoc downloads of this repo 353# Disable adhoc downloads of this repo
346repo.snapshots=0 354repo.snapshots=0
347 355
348# Disable line-counts for this repo 356# Disable line-counts for this repo
349repo.enable-log-linecount=0 357repo.enable-log-linecount=0
350 358
351 359
352BUGS 360BUGS
353---- 361----
354Comments currently cannot appear on the same line as a setting; the comment 362Comments currently cannot appear on the same line as a setting; the comment
355will be included as part of the value. E.g. this line: 363will be included as part of the value. E.g. this line:
356 364
357 robots=index # allow indexing 365 robots=index # allow indexing
358 366
359will generate the following html element: 367will generate the following html element:
360 368
361 <meta name='robots' content='index # allow indexing'/> 369 <meta name='robots' content='index # allow indexing'/>
362 370
363 371
364 372
365AUTHOR 373AUTHOR
366------ 374------
367Lars Hjemli <hjemli@gmail.com> 375Lars Hjemli <hjemli@gmail.com>
diff --git a/cmd.c b/cmd.c
index 5b3c14c..744bf84 100644
--- a/cmd.c
+++ b/cmd.c
@@ -1,165 +1,175 @@
1/* cmd.c: the cgit command dispatcher 1/* cmd.c: the cgit command dispatcher
2 * 2 *
3 * Copyright (C) 2008 Lars Hjemli 3 * Copyright (C) 2008 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "cmd.h" 10#include "cmd.h"
11#include "cache.h" 11#include "cache.h"
12#include "ui-shared.h" 12#include "ui-shared.h"
13#include "ui-atom.h" 13#include "ui-atom.h"
14#include "ui-blob.h" 14#include "ui-blob.h"
15#include "ui-clone.h" 15#include "ui-clone.h"
16#include "ui-commit.h" 16#include "ui-commit.h"
17#include "ui-diff.h" 17#include "ui-diff.h"
18#include "ui-log.h" 18#include "ui-log.h"
19#include "ui-patch.h" 19#include "ui-patch.h"
20#include "ui-plain.h" 20#include "ui-plain.h"
21#include "ui-refs.h" 21#include "ui-refs.h"
22#include "ui-repolist.h" 22#include "ui-repolist.h"
23#include "ui-snapshot.h" 23#include "ui-snapshot.h"
24#include "ui-stats.h"
24#include "ui-summary.h" 25#include "ui-summary.h"
25#include "ui-tag.h" 26#include "ui-tag.h"
26#include "ui-tree.h" 27#include "ui-tree.h"
27 28
28static void HEAD_fn(struct cgit_context *ctx) 29static void HEAD_fn(struct cgit_context *ctx)
29{ 30{
30 cgit_clone_head(ctx); 31 cgit_clone_head(ctx);
31} 32}
32 33
33static void atom_fn(struct cgit_context *ctx) 34static void atom_fn(struct cgit_context *ctx)
34{ 35{
35 cgit_print_atom(ctx->qry.head, ctx->qry.path, 10); 36 cgit_print_atom(ctx->qry.head, ctx->qry.path, 10);
36} 37}
37 38
38static void about_fn(struct cgit_context *ctx) 39static void about_fn(struct cgit_context *ctx)
39{ 40{
40 if (ctx->repo) 41 if (ctx->repo)
41 cgit_print_repo_readme(); 42 cgit_print_repo_readme();
42 else 43 else
43 cgit_print_site_readme(); 44 cgit_print_site_readme();
44} 45}
45 46
46static void blob_fn(struct cgit_context *ctx) 47static void blob_fn(struct cgit_context *ctx)
47{ 48{
48 cgit_print_blob(ctx->qry.sha1, ctx->qry.path, ctx->qry.head); 49 cgit_print_blob(ctx->qry.sha1, ctx->qry.path, ctx->qry.head);
49} 50}
50 51
51static void commit_fn(struct cgit_context *ctx) 52static void commit_fn(struct cgit_context *ctx)
52{ 53{
53 cgit_print_commit(ctx->qry.sha1); 54 cgit_print_commit(ctx->qry.sha1);
54} 55}
55 56
56static void diff_fn(struct cgit_context *ctx) 57static void diff_fn(struct cgit_context *ctx)
57{ 58{
58 cgit_print_diff(ctx->qry.sha1, ctx->qry.sha2, ctx->qry.path); 59 cgit_print_diff(ctx->qry.sha1, ctx->qry.sha2, ctx->qry.path);
59} 60}
60 61
61static void info_fn(struct cgit_context *ctx) 62static void info_fn(struct cgit_context *ctx)
62{ 63{
63 cgit_clone_info(ctx); 64 cgit_clone_info(ctx);
64} 65}
65 66
66static void log_fn(struct cgit_context *ctx) 67static void log_fn(struct cgit_context *ctx)
67{ 68{
68 cgit_print_log(ctx->qry.sha1, ctx->qry.ofs, ctx->cfg.max_commit_count, 69 cgit_print_log(ctx->qry.sha1, ctx->qry.ofs, ctx->cfg.max_commit_count,
69 ctx->qry.grep, ctx->qry.search, ctx->qry.path, 1); 70 ctx->qry.grep, ctx->qry.search, ctx->qry.path, 1);
70} 71}
71 72
72static void ls_cache_fn(struct cgit_context *ctx) 73static void ls_cache_fn(struct cgit_context *ctx)
73{ 74{
74 ctx->page.mimetype = "text/plain"; 75 ctx->page.mimetype = "text/plain";
75 ctx->page.filename = "ls-cache.txt"; 76 ctx->page.filename = "ls-cache.txt";
76 cgit_print_http_headers(ctx); 77 cgit_print_http_headers(ctx);
77 cache_ls(ctx->cfg.cache_root); 78 cache_ls(ctx->cfg.cache_root);
78} 79}
79 80
80static void objects_fn(struct cgit_context *ctx) 81static void objects_fn(struct cgit_context *ctx)
81{ 82{
82 cgit_clone_objects(ctx); 83 cgit_clone_objects(ctx);
83} 84}
84 85
85static void repolist_fn(struct cgit_context *ctx) 86static void repolist_fn(struct cgit_context *ctx)
86{ 87{
87 cgit_print_repolist(); 88 cgit_print_repolist();
88} 89}
89 90
90static void patch_fn(struct cgit_context *ctx) 91static void patch_fn(struct cgit_context *ctx)
91{ 92{
92 cgit_print_patch(ctx->qry.sha1); 93 cgit_print_patch(ctx->qry.sha1);
93} 94}
94 95
95static void plain_fn(struct cgit_context *ctx) 96static void plain_fn(struct cgit_context *ctx)
96{ 97{
97 cgit_print_plain(ctx); 98 cgit_print_plain(ctx);
98} 99}
99 100
100static void refs_fn(struct cgit_context *ctx) 101static void refs_fn(struct cgit_context *ctx)
101{ 102{
102 cgit_print_refs(); 103 cgit_print_refs();
103} 104}
104 105
105static void snapshot_fn(struct cgit_context *ctx) 106static void snapshot_fn(struct cgit_context *ctx)
106{ 107{
107 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, 108 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1,
108 cgit_repobasename(ctx->repo->url), ctx->qry.path, 109 cgit_repobasename(ctx->repo->url), ctx->qry.path,
109 ctx->repo->snapshots, ctx->qry.nohead); 110 ctx->repo->snapshots, ctx->qry.nohead);
110} 111}
111 112
113static void stats_fn(struct cgit_context *ctx)
114{
115 if (ctx->repo->enable_stats)
116 cgit_show_stats(ctx);
117 else
118 cgit_print_error("Stats disabled for this repo");
119}
120
112static void summary_fn(struct cgit_context *ctx) 121static void summary_fn(struct cgit_context *ctx)
113{ 122{
114 cgit_print_summary(); 123 cgit_print_summary();
115} 124}
116 125
117static void tag_fn(struct cgit_context *ctx) 126static void tag_fn(struct cgit_context *ctx)
118{ 127{
119 cgit_print_tag(ctx->qry.sha1); 128 cgit_print_tag(ctx->qry.sha1);
120} 129}
121 130
122static void tree_fn(struct cgit_context *ctx) 131static void tree_fn(struct cgit_context *ctx)
123{ 132{
124 cgit_print_tree(ctx->qry.sha1, ctx->qry.path); 133 cgit_print_tree(ctx->qry.sha1, ctx->qry.path);
125} 134}
126 135
127#define def_cmd(name, want_repo, want_layout) \ 136#define def_cmd(name, want_repo, want_layout) \
128 {#name, name##_fn, want_repo, want_layout} 137 {#name, name##_fn, want_repo, want_layout}
129 138
130struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx) 139struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx)
131{ 140{
132 static struct cgit_cmd cmds[] = { 141 static struct cgit_cmd cmds[] = {
133 def_cmd(HEAD, 1, 0), 142 def_cmd(HEAD, 1, 0),
134 def_cmd(atom, 1, 0), 143 def_cmd(atom, 1, 0),
135 def_cmd(about, 0, 1), 144 def_cmd(about, 0, 1),
136 def_cmd(blob, 1, 0), 145 def_cmd(blob, 1, 0),
137 def_cmd(commit, 1, 1), 146 def_cmd(commit, 1, 1),
138 def_cmd(diff, 1, 1), 147 def_cmd(diff, 1, 1),
139 def_cmd(info, 1, 0), 148 def_cmd(info, 1, 0),
140 def_cmd(log, 1, 1), 149 def_cmd(log, 1, 1),
141 def_cmd(ls_cache, 0, 0), 150 def_cmd(ls_cache, 0, 0),
142 def_cmd(objects, 1, 0), 151 def_cmd(objects, 1, 0),
143 def_cmd(patch, 1, 0), 152 def_cmd(patch, 1, 0),
144 def_cmd(plain, 1, 0), 153 def_cmd(plain, 1, 0),
145 def_cmd(refs, 1, 1), 154 def_cmd(refs, 1, 1),
146 def_cmd(repolist, 0, 0), 155 def_cmd(repolist, 0, 0),
147 def_cmd(snapshot, 1, 0), 156 def_cmd(snapshot, 1, 0),
157 def_cmd(stats, 1, 1),
148 def_cmd(summary, 1, 1), 158 def_cmd(summary, 1, 1),
149 def_cmd(tag, 1, 1), 159 def_cmd(tag, 1, 1),
150 def_cmd(tree, 1, 1), 160 def_cmd(tree, 1, 1),
151 }; 161 };
152 int i; 162 int i;
153 163
154 if (ctx->qry.page == NULL) { 164 if (ctx->qry.page == NULL) {
155 if (ctx->repo) 165 if (ctx->repo)
156 ctx->qry.page = "summary"; 166 ctx->qry.page = "summary";
157 else 167 else
158 ctx->qry.page = "repolist"; 168 ctx->qry.page = "repolist";
159 } 169 }
160 170
161 for(i = 0; i < sizeof(cmds)/sizeof(*cmds); i++) 171 for(i = 0; i < sizeof(cmds)/sizeof(*cmds); i++)
162 if (!strcmp(ctx->qry.page, cmds[i].name)) 172 if (!strcmp(ctx->qry.page, cmds[i].name))
163 return &cmds[i]; 173 return &cmds[i];
164 return NULL; 174 return NULL;
165} 175}
diff --git a/shared.c b/shared.c
index f5875e4..37333f0 100644
--- a/shared.c
+++ b/shared.c
@@ -1,344 +1,345 @@
1/* shared.c: global vars + some callback functions 1/* shared.c: global vars + some callback functions
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10 10
11struct cgit_repolist cgit_repolist; 11struct cgit_repolist cgit_repolist;
12struct cgit_context ctx; 12struct cgit_context ctx;
13int cgit_cmd; 13int cgit_cmd;
14 14
15int chk_zero(int result, char *msg) 15int chk_zero(int result, char *msg)
16{ 16{
17 if (result != 0) 17 if (result != 0)
18 die("%s: %s", msg, strerror(errno)); 18 die("%s: %s", msg, strerror(errno));
19 return result; 19 return result;
20} 20}
21 21
22int chk_positive(int result, char *msg) 22int chk_positive(int result, char *msg)
23{ 23{
24 if (result <= 0) 24 if (result <= 0)
25 die("%s: %s", msg, strerror(errno)); 25 die("%s: %s", msg, strerror(errno));
26 return result; 26 return result;
27} 27}
28 28
29int chk_non_negative(int result, char *msg) 29int chk_non_negative(int result, char *msg)
30{ 30{
31 if (result < 0) 31 if (result < 0)
32 die("%s: %s",msg, strerror(errno)); 32 die("%s: %s",msg, strerror(errno));
33 return result; 33 return result;
34} 34}
35 35
36struct cgit_repo *cgit_add_repo(const char *url) 36struct cgit_repo *cgit_add_repo(const char *url)
37{ 37{
38 struct cgit_repo *ret; 38 struct cgit_repo *ret;
39 39
40 if (++cgit_repolist.count > cgit_repolist.length) { 40 if (++cgit_repolist.count > cgit_repolist.length) {
41 if (cgit_repolist.length == 0) 41 if (cgit_repolist.length == 0)
42 cgit_repolist.length = 8; 42 cgit_repolist.length = 8;
43 else 43 else
44 cgit_repolist.length *= 2; 44 cgit_repolist.length *= 2;
45 cgit_repolist.repos = xrealloc(cgit_repolist.repos, 45 cgit_repolist.repos = xrealloc(cgit_repolist.repos,
46 cgit_repolist.length * 46 cgit_repolist.length *
47 sizeof(struct cgit_repo)); 47 sizeof(struct cgit_repo));
48 } 48 }
49 49
50 ret = &cgit_repolist.repos[cgit_repolist.count-1]; 50 ret = &cgit_repolist.repos[cgit_repolist.count-1];
51 ret->url = trim_end(url, '/'); 51 ret->url = trim_end(url, '/');
52 ret->name = ret->url; 52 ret->name = ret->url;
53 ret->path = NULL; 53 ret->path = NULL;
54 ret->desc = "[no description]"; 54 ret->desc = "[no description]";
55 ret->owner = NULL; 55 ret->owner = NULL;
56 ret->group = ctx.cfg.repo_group; 56 ret->group = ctx.cfg.repo_group;
57 ret->defbranch = "master"; 57 ret->defbranch = "master";
58 ret->snapshots = ctx.cfg.snapshots; 58 ret->snapshots = ctx.cfg.snapshots;
59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount; 59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount; 60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
61 ret->enable_stats = ctx.cfg.enable_stats;
61 ret->module_link = ctx.cfg.module_link; 62 ret->module_link = ctx.cfg.module_link;
62 ret->readme = NULL; 63 ret->readme = NULL;
63 return ret; 64 return ret;
64} 65}
65 66
66struct cgit_repo *cgit_get_repoinfo(const char *url) 67struct cgit_repo *cgit_get_repoinfo(const char *url)
67{ 68{
68 int i; 69 int i;
69 struct cgit_repo *repo; 70 struct cgit_repo *repo;
70 71
71 for (i=0; i<cgit_repolist.count; i++) { 72 for (i=0; i<cgit_repolist.count; i++) {
72 repo = &cgit_repolist.repos[i]; 73 repo = &cgit_repolist.repos[i];
73 if (!strcmp(repo->url, url)) 74 if (!strcmp(repo->url, url))
74 return repo; 75 return repo;
75 } 76 }
76 return NULL; 77 return NULL;
77} 78}
78 79
79void *cgit_free_commitinfo(struct commitinfo *info) 80void *cgit_free_commitinfo(struct commitinfo *info)
80{ 81{
81 free(info->author); 82 free(info->author);
82 free(info->author_email); 83 free(info->author_email);
83 free(info->committer); 84 free(info->committer);
84 free(info->committer_email); 85 free(info->committer_email);
85 free(info->subject); 86 free(info->subject);
86 free(info->msg); 87 free(info->msg);
87 free(info->msg_encoding); 88 free(info->msg_encoding);
88 free(info); 89 free(info);
89 return NULL; 90 return NULL;
90} 91}
91 92
92char *trim_end(const char *str, char c) 93char *trim_end(const char *str, char c)
93{ 94{
94 int len; 95 int len;
95 char *s, *t; 96 char *s, *t;
96 97
97 if (str == NULL) 98 if (str == NULL)
98 return NULL; 99 return NULL;
99 t = (char *)str; 100 t = (char *)str;
100 len = strlen(t); 101 len = strlen(t);
101 while(len > 0 && t[len - 1] == c) 102 while(len > 0 && t[len - 1] == c)
102 len--; 103 len--;
103 104
104 if (len == 0) 105 if (len == 0)
105 return NULL; 106 return NULL;
106 107
107 c = t[len]; 108 c = t[len];
108 t[len] = '\0'; 109 t[len] = '\0';
109 s = xstrdup(t); 110 s = xstrdup(t);
110 t[len] = c; 111 t[len] = c;
111 return s; 112 return s;
112} 113}
113 114
114char *strlpart(char *txt, int maxlen) 115char *strlpart(char *txt, int maxlen)
115{ 116{
116 char *result; 117 char *result;
117 118
118 if (!txt) 119 if (!txt)
119 return txt; 120 return txt;
120 121
121 if (strlen(txt) <= maxlen) 122 if (strlen(txt) <= maxlen)
122 return txt; 123 return txt;
123 result = xmalloc(maxlen + 1); 124 result = xmalloc(maxlen + 1);
124 memcpy(result, txt, maxlen - 3); 125 memcpy(result, txt, maxlen - 3);
125 result[maxlen-1] = result[maxlen-2] = result[maxlen-3] = '.'; 126 result[maxlen-1] = result[maxlen-2] = result[maxlen-3] = '.';
126 result[maxlen] = '\0'; 127 result[maxlen] = '\0';
127 return result; 128 return result;
128} 129}
129 130
130char *strrpart(char *txt, int maxlen) 131char *strrpart(char *txt, int maxlen)
131{ 132{
132 char *result; 133 char *result;
133 134
134 if (!txt) 135 if (!txt)
135 return txt; 136 return txt;
136 137
137 if (strlen(txt) <= maxlen) 138 if (strlen(txt) <= maxlen)
138 return txt; 139 return txt;
139 result = xmalloc(maxlen + 1); 140 result = xmalloc(maxlen + 1);
140 memcpy(result + 3, txt + strlen(txt) - maxlen + 4, maxlen - 3); 141 memcpy(result + 3, txt + strlen(txt) - maxlen + 4, maxlen - 3);
141 result[0] = result[1] = result[2] = '.'; 142 result[0] = result[1] = result[2] = '.';
142 return result; 143 return result;
143} 144}
144 145
145void cgit_add_ref(struct reflist *list, struct refinfo *ref) 146void cgit_add_ref(struct reflist *list, struct refinfo *ref)
146{ 147{
147 size_t size; 148 size_t size;
148 149
149 if (list->count >= list->alloc) { 150 if (list->count >= list->alloc) {
150 list->alloc += (list->alloc ? list->alloc : 4); 151 list->alloc += (list->alloc ? list->alloc : 4);
151 size = list->alloc * sizeof(struct refinfo *); 152 size = list->alloc * sizeof(struct refinfo *);
152 list->refs = xrealloc(list->refs, size); 153 list->refs = xrealloc(list->refs, size);
153 } 154 }
154 list->refs[list->count++] = ref; 155 list->refs[list->count++] = ref;
155} 156}
156 157
157struct refinfo *cgit_mk_refinfo(const char *refname, const unsigned char *sha1) 158struct refinfo *cgit_mk_refinfo(const char *refname, const unsigned char *sha1)
158{ 159{
159 struct refinfo *ref; 160 struct refinfo *ref;
160 161
161 ref = xmalloc(sizeof (struct refinfo)); 162 ref = xmalloc(sizeof (struct refinfo));
162 ref->refname = xstrdup(refname); 163 ref->refname = xstrdup(refname);
163 ref->object = parse_object(sha1); 164 ref->object = parse_object(sha1);
164 switch (ref->object->type) { 165 switch (ref->object->type) {
165 case OBJ_TAG: 166 case OBJ_TAG:
166 ref->tag = cgit_parse_tag((struct tag *)ref->object); 167 ref->tag = cgit_parse_tag((struct tag *)ref->object);
167 break; 168 break;
168 case OBJ_COMMIT: 169 case OBJ_COMMIT:
169 ref->commit = cgit_parse_commit((struct commit *)ref->object); 170 ref->commit = cgit_parse_commit((struct commit *)ref->object);
170 break; 171 break;
171 } 172 }
172 return ref; 173 return ref;
173} 174}
174 175
175int cgit_refs_cb(const char *refname, const unsigned char *sha1, int flags, 176int cgit_refs_cb(const char *refname, const unsigned char *sha1, int flags,
176 void *cb_data) 177 void *cb_data)
177{ 178{
178 struct reflist *list = (struct reflist *)cb_data; 179 struct reflist *list = (struct reflist *)cb_data;
179 struct refinfo *info = cgit_mk_refinfo(refname, sha1); 180 struct refinfo *info = cgit_mk_refinfo(refname, sha1);
180 181
181 if (info) 182 if (info)
182 cgit_add_ref(list, info); 183 cgit_add_ref(list, info);
183 return 0; 184 return 0;
184} 185}
185 186
186void cgit_diff_tree_cb(struct diff_queue_struct *q, 187void cgit_diff_tree_cb(struct diff_queue_struct *q,
187 struct diff_options *options, void *data) 188 struct diff_options *options, void *data)
188{ 189{
189 int i; 190 int i;
190 191
191 for (i = 0; i < q->nr; i++) { 192 for (i = 0; i < q->nr; i++) {
192 if (q->queue[i]->status == 'U') 193 if (q->queue[i]->status == 'U')
193 continue; 194 continue;
194 ((filepair_fn)data)(q->queue[i]); 195 ((filepair_fn)data)(q->queue[i]);
195 } 196 }
196} 197}
197 198
198static int load_mmfile(mmfile_t *file, const unsigned char *sha1) 199static int load_mmfile(mmfile_t *file, const unsigned char *sha1)
199{ 200{
200 enum object_type type; 201 enum object_type type;
201 202
202 if (is_null_sha1(sha1)) { 203 if (is_null_sha1(sha1)) {
203 file->ptr = (char *)""; 204 file->ptr = (char *)"";
204 file->size = 0; 205 file->size = 0;
205 } else { 206 } else {
206 file->ptr = read_sha1_file(sha1, &type, 207 file->ptr = read_sha1_file(sha1, &type,
207 (unsigned long *)&file->size); 208 (unsigned long *)&file->size);
208 } 209 }
209 return 1; 210 return 1;
210} 211}
211 212
212/* 213/*
213 * Receive diff-buffers from xdiff and concatenate them as 214 * Receive diff-buffers from xdiff and concatenate them as
214 * needed across multiple callbacks. 215 * needed across multiple callbacks.
215 * 216 *
216 * This is basically a copy of xdiff-interface.c/xdiff_outf(), 217 * This is basically a copy of xdiff-interface.c/xdiff_outf(),
217 * ripped from git and modified to use globals instead of 218 * ripped from git and modified to use globals instead of
218 * a special callback-struct. 219 * a special callback-struct.
219 */ 220 */
220char *diffbuf = NULL; 221char *diffbuf = NULL;
221int buflen = 0; 222int buflen = 0;
222 223
223int filediff_cb(void *priv, mmbuffer_t *mb, int nbuf) 224int filediff_cb(void *priv, mmbuffer_t *mb, int nbuf)
224{ 225{
225 int i; 226 int i;
226 227
227 for (i = 0; i < nbuf; i++) { 228 for (i = 0; i < nbuf; i++) {
228 if (mb[i].ptr[mb[i].size-1] != '\n') { 229 if (mb[i].ptr[mb[i].size-1] != '\n') {
229 /* Incomplete line */ 230 /* Incomplete line */
230 diffbuf = xrealloc(diffbuf, buflen + mb[i].size); 231 diffbuf = xrealloc(diffbuf, buflen + mb[i].size);
231 memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size); 232 memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size);
232 buflen += mb[i].size; 233 buflen += mb[i].size;
233 continue; 234 continue;
234 } 235 }
235 236
236 /* we have a complete line */ 237 /* we have a complete line */
237 if (!diffbuf) { 238 if (!diffbuf) {
238 ((linediff_fn)priv)(mb[i].ptr, mb[i].size); 239 ((linediff_fn)priv)(mb[i].ptr, mb[i].size);
239 continue; 240 continue;
240 } 241 }
241 diffbuf = xrealloc(diffbuf, buflen + mb[i].size); 242 diffbuf = xrealloc(diffbuf, buflen + mb[i].size);
242 memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size); 243 memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size);
243 ((linediff_fn)priv)(diffbuf, buflen + mb[i].size); 244 ((linediff_fn)priv)(diffbuf, buflen + mb[i].size);
244 free(diffbuf); 245 free(diffbuf);
245 diffbuf = NULL; 246 diffbuf = NULL;
246 buflen = 0; 247 buflen = 0;
247 } 248 }
248 if (diffbuf) { 249 if (diffbuf) {
249 ((linediff_fn)priv)(diffbuf, buflen); 250 ((linediff_fn)priv)(diffbuf, buflen);
250 free(diffbuf); 251 free(diffbuf);
251 diffbuf = NULL; 252 diffbuf = NULL;
252 buflen = 0; 253 buflen = 0;
253 } 254 }
254 return 0; 255 return 0;
255} 256}
256 257
257int cgit_diff_files(const unsigned char *old_sha1, 258int cgit_diff_files(const unsigned char *old_sha1,
258 const unsigned char *new_sha1, 259 const unsigned char *new_sha1,
259 linediff_fn fn) 260 linediff_fn fn)
260{ 261{
261 mmfile_t file1, file2; 262 mmfile_t file1, file2;
262 xpparam_t diff_params; 263 xpparam_t diff_params;
263 xdemitconf_t emit_params; 264 xdemitconf_t emit_params;
264 xdemitcb_t emit_cb; 265 xdemitcb_t emit_cb;
265 266
266 if (!load_mmfile(&file1, old_sha1) || !load_mmfile(&file2, new_sha1)) 267 if (!load_mmfile(&file1, old_sha1) || !load_mmfile(&file2, new_sha1))
267 return 1; 268 return 1;
268 269
269 diff_params.flags = XDF_NEED_MINIMAL; 270 diff_params.flags = XDF_NEED_MINIMAL;
270 emit_params.ctxlen = 3; 271 emit_params.ctxlen = 3;
271 emit_params.flags = XDL_EMIT_FUNCNAMES; 272 emit_params.flags = XDL_EMIT_FUNCNAMES;
272 emit_params.find_func = NULL; 273 emit_params.find_func = NULL;
273 emit_cb.outf = filediff_cb; 274 emit_cb.outf = filediff_cb;
274 emit_cb.priv = fn; 275 emit_cb.priv = fn;
275 xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb); 276 xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb);
276 return 0; 277 return 0;
277} 278}
278 279
279void cgit_diff_tree(const unsigned char *old_sha1, 280void cgit_diff_tree(const unsigned char *old_sha1,
280 const unsigned char *new_sha1, 281 const unsigned char *new_sha1,
281 filepair_fn fn, const char *prefix) 282 filepair_fn fn, const char *prefix)
282{ 283{
283 struct diff_options opt; 284 struct diff_options opt;
284 int ret; 285 int ret;
285 int prefixlen; 286 int prefixlen;
286 287
287 diff_setup(&opt); 288 diff_setup(&opt);
288 opt.output_format = DIFF_FORMAT_CALLBACK; 289 opt.output_format = DIFF_FORMAT_CALLBACK;
289 opt.detect_rename = 1; 290 opt.detect_rename = 1;
290 opt.rename_limit = ctx.cfg.renamelimit; 291 opt.rename_limit = ctx.cfg.renamelimit;
291 DIFF_OPT_SET(&opt, RECURSIVE); 292 DIFF_OPT_SET(&opt, RECURSIVE);
292 opt.format_callback = cgit_diff_tree_cb; 293 opt.format_callback = cgit_diff_tree_cb;
293 opt.format_callback_data = fn; 294 opt.format_callback_data = fn;
294 if (prefix) { 295 if (prefix) {
295 opt.nr_paths = 1; 296 opt.nr_paths = 1;
296 opt.paths = &prefix; 297 opt.paths = &prefix;
297 prefixlen = strlen(prefix); 298 prefixlen = strlen(prefix);
298 opt.pathlens = &prefixlen; 299 opt.pathlens = &prefixlen;
299 } 300 }
300 diff_setup_done(&opt); 301 diff_setup_done(&opt);
301 302
302 if (old_sha1 && !is_null_sha1(old_sha1)) 303 if (old_sha1 && !is_null_sha1(old_sha1))
303 ret = diff_tree_sha1(old_sha1, new_sha1, "", &opt); 304 ret = diff_tree_sha1(old_sha1, new_sha1, "", &opt);
304 else 305 else
305 ret = diff_root_tree_sha1(new_sha1, "", &opt); 306 ret = diff_root_tree_sha1(new_sha1, "", &opt);
306 diffcore_std(&opt); 307 diffcore_std(&opt);
307 diff_flush(&opt); 308 diff_flush(&opt);
308} 309}
309 310
310void cgit_diff_commit(struct commit *commit, filepair_fn fn) 311void cgit_diff_commit(struct commit *commit, filepair_fn fn)
311{ 312{
312 unsigned char *old_sha1 = NULL; 313 unsigned char *old_sha1 = NULL;
313 314
314 if (commit->parents) 315 if (commit->parents)
315 old_sha1 = commit->parents->item->object.sha1; 316 old_sha1 = commit->parents->item->object.sha1;
316 cgit_diff_tree(old_sha1, commit->object.sha1, fn, NULL); 317 cgit_diff_tree(old_sha1, commit->object.sha1, fn, NULL);
317} 318}
318 319
319int cgit_parse_snapshots_mask(const char *str) 320int cgit_parse_snapshots_mask(const char *str)
320{ 321{
321 const struct cgit_snapshot_format *f; 322 const struct cgit_snapshot_format *f;
322 static const char *delim = " \t,:/|;"; 323 static const char *delim = " \t,:/|;";
323 int tl, sl, rv = 0; 324 int tl, sl, rv = 0;
324 325
325 /* favor legacy setting */ 326 /* favor legacy setting */
326 if(atoi(str)) 327 if(atoi(str))
327 return 1; 328 return 1;
328 for(;;) { 329 for(;;) {
329 str += strspn(str,delim); 330 str += strspn(str,delim);
330 tl = strcspn(str,delim); 331 tl = strcspn(str,delim);
331 if (!tl) 332 if (!tl)
332 break; 333 break;
333 for (f = cgit_snapshot_formats; f->suffix; f++) { 334 for (f = cgit_snapshot_formats; f->suffix; f++) {
334 sl = strlen(f->suffix); 335 sl = strlen(f->suffix);
335 if((tl == sl && !strncmp(f->suffix, str, tl)) || 336 if((tl == sl && !strncmp(f->suffix, str, tl)) ||
336 (tl == sl-1 && !strncmp(f->suffix+1, str, tl-1))) { 337 (tl == sl-1 && !strncmp(f->suffix+1, str, tl-1))) {
337 rv |= f->bit; 338 rv |= f->bit;
338 break; 339 break;
339 } 340 }
340 } 341 }
341 str += tl; 342 str += tl;
342 } 343 }
343 return rv; 344 return rv;
344} 345}
diff --git a/ui-shared.c b/ui-shared.c
index 224e5f3..0e688a0 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -260,455 +260,458 @@ static void reporevlink(char *page, char *name, char *title, char *class,
260void cgit_summary_link(char *name, char *title, char *class, char *head) 260void cgit_summary_link(char *name, char *title, char *class, char *head)
261{ 261{
262 reporevlink(NULL, name, title, class, head, NULL, NULL); 262 reporevlink(NULL, name, title, class, head, NULL, NULL);
263} 263}
264 264
265void cgit_tag_link(char *name, char *title, char *class, char *head, 265void cgit_tag_link(char *name, char *title, char *class, char *head,
266 char *rev) 266 char *rev)
267{ 267{
268 reporevlink("tag", name, title, class, head, rev, NULL); 268 reporevlink("tag", name, title, class, head, rev, NULL);
269} 269}
270 270
271void cgit_tree_link(char *name, char *title, char *class, char *head, 271void cgit_tree_link(char *name, char *title, char *class, char *head,
272 char *rev, char *path) 272 char *rev, char *path)
273{ 273{
274 reporevlink("tree", name, title, class, head, rev, path); 274 reporevlink("tree", name, title, class, head, rev, path);
275} 275}
276 276
277void cgit_plain_link(char *name, char *title, char *class, char *head, 277void cgit_plain_link(char *name, char *title, char *class, char *head,
278 char *rev, char *path) 278 char *rev, char *path)
279{ 279{
280 reporevlink("plain", name, title, class, head, rev, path); 280 reporevlink("plain", name, title, class, head, rev, path);
281} 281}
282 282
283void cgit_log_link(char *name, char *title, char *class, char *head, 283void cgit_log_link(char *name, char *title, char *class, char *head,
284 char *rev, char *path, int ofs, char *grep, char *pattern) 284 char *rev, char *path, int ofs, char *grep, char *pattern)
285{ 285{
286 char *delim; 286 char *delim;
287 287
288 delim = repolink(title, class, "log", head, path); 288 delim = repolink(title, class, "log", head, path);
289 if (rev && strcmp(rev, ctx.qry.head)) { 289 if (rev && strcmp(rev, ctx.qry.head)) {
290 html(delim); 290 html(delim);
291 html("id="); 291 html("id=");
292 html_url_arg(rev); 292 html_url_arg(rev);
293 delim = "&"; 293 delim = "&";
294 } 294 }
295 if (grep && pattern) { 295 if (grep && pattern) {
296 html(delim); 296 html(delim);
297 html("qt="); 297 html("qt=");
298 html_url_arg(grep); 298 html_url_arg(grep);
299 delim = "&"; 299 delim = "&";
300 html(delim); 300 html(delim);
301 html("q="); 301 html("q=");
302 html_url_arg(pattern); 302 html_url_arg(pattern);
303 } 303 }
304 if (ofs > 0) { 304 if (ofs > 0) {
305 html(delim); 305 html(delim);
306 html("ofs="); 306 html("ofs=");
307 htmlf("%d", ofs); 307 htmlf("%d", ofs);
308 } 308 }
309 html("'>"); 309 html("'>");
310 html_txt(name); 310 html_txt(name);
311 html("</a>"); 311 html("</a>");
312} 312}
313 313
314void cgit_commit_link(char *name, char *title, char *class, char *head, 314void cgit_commit_link(char *name, char *title, char *class, char *head,
315 char *rev) 315 char *rev)
316{ 316{
317 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) { 317 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
318 name[ctx.cfg.max_msg_len] = '\0'; 318 name[ctx.cfg.max_msg_len] = '\0';
319 name[ctx.cfg.max_msg_len - 1] = '.'; 319 name[ctx.cfg.max_msg_len - 1] = '.';
320 name[ctx.cfg.max_msg_len - 2] = '.'; 320 name[ctx.cfg.max_msg_len - 2] = '.';
321 name[ctx.cfg.max_msg_len - 3] = '.'; 321 name[ctx.cfg.max_msg_len - 3] = '.';
322 } 322 }
323 reporevlink("commit", name, title, class, head, rev, NULL); 323 reporevlink("commit", name, title, class, head, rev, NULL);
324} 324}
325 325
326void cgit_refs_link(char *name, char *title, char *class, char *head, 326void cgit_refs_link(char *name, char *title, char *class, char *head,
327 char *rev, char *path) 327 char *rev, char *path)
328{ 328{
329 reporevlink("refs", name, title, class, head, rev, path); 329 reporevlink("refs", name, title, class, head, rev, path);
330} 330}
331 331
332void cgit_snapshot_link(char *name, char *title, char *class, char *head, 332void cgit_snapshot_link(char *name, char *title, char *class, char *head,
333 char *rev, char *archivename) 333 char *rev, char *archivename)
334{ 334{
335 reporevlink("snapshot", name, title, class, head, rev, archivename); 335 reporevlink("snapshot", name, title, class, head, rev, archivename);
336} 336}
337 337
338void cgit_diff_link(char *name, char *title, char *class, char *head, 338void cgit_diff_link(char *name, char *title, char *class, char *head,
339 char *new_rev, char *old_rev, char *path) 339 char *new_rev, char *old_rev, char *path)
340{ 340{
341 char *delim; 341 char *delim;
342 342
343 delim = repolink(title, class, "diff", head, path); 343 delim = repolink(title, class, "diff", head, path);
344 if (new_rev && strcmp(new_rev, ctx.qry.head)) { 344 if (new_rev && strcmp(new_rev, ctx.qry.head)) {
345 html(delim); 345 html(delim);
346 html("id="); 346 html("id=");
347 html_url_arg(new_rev); 347 html_url_arg(new_rev);
348 delim = "&amp;"; 348 delim = "&amp;";
349 } 349 }
350 if (old_rev) { 350 if (old_rev) {
351 html(delim); 351 html(delim);
352 html("id2="); 352 html("id2=");
353 html_url_arg(old_rev); 353 html_url_arg(old_rev);
354 } 354 }
355 html("'>"); 355 html("'>");
356 html_txt(name); 356 html_txt(name);
357 html("</a>"); 357 html("</a>");
358} 358}
359 359
360void cgit_patch_link(char *name, char *title, char *class, char *head, 360void cgit_patch_link(char *name, char *title, char *class, char *head,
361 char *rev) 361 char *rev)
362{ 362{
363 reporevlink("patch", name, title, class, head, rev, NULL); 363 reporevlink("patch", name, title, class, head, rev, NULL);
364} 364}
365 365
366void cgit_object_link(struct object *obj) 366void cgit_object_link(struct object *obj)
367{ 367{
368 char *page, *rev, *name; 368 char *page, *rev, *name;
369 369
370 if (obj->type == OBJ_COMMIT) { 370 if (obj->type == OBJ_COMMIT) {
371 cgit_commit_link(fmt("commit %s", sha1_to_hex(obj->sha1)), NULL, NULL, 371 cgit_commit_link(fmt("commit %s", sha1_to_hex(obj->sha1)), NULL, NULL,
372 ctx.qry.head, sha1_to_hex(obj->sha1)); 372 ctx.qry.head, sha1_to_hex(obj->sha1));
373 return; 373 return;
374 } else if (obj->type == OBJ_TREE) 374 } else if (obj->type == OBJ_TREE)
375 page = "tree"; 375 page = "tree";
376 else if (obj->type == OBJ_TAG) 376 else if (obj->type == OBJ_TAG)
377 page = "tag"; 377 page = "tag";
378 else 378 else
379 page = "blob"; 379 page = "blob";
380 rev = sha1_to_hex(obj->sha1); 380 rev = sha1_to_hex(obj->sha1);
381 name = fmt("%s %s", typename(obj->type), rev); 381 name = fmt("%s %s", typename(obj->type), rev);
382 reporevlink(page, name, NULL, NULL, ctx.qry.head, rev, NULL); 382 reporevlink(page, name, NULL, NULL, ctx.qry.head, rev, NULL);
383} 383}
384 384
385void cgit_print_date(time_t secs, char *format, int local_time) 385void cgit_print_date(time_t secs, char *format, int local_time)
386{ 386{
387 char buf[64]; 387 char buf[64];
388 struct tm *time; 388 struct tm *time;
389 389
390 if (!secs) 390 if (!secs)
391 return; 391 return;
392 if(local_time) 392 if(local_time)
393 time = localtime(&secs); 393 time = localtime(&secs);
394 else 394 else
395 time = gmtime(&secs); 395 time = gmtime(&secs);
396 strftime(buf, sizeof(buf)-1, format, time); 396 strftime(buf, sizeof(buf)-1, format, time);
397 html_txt(buf); 397 html_txt(buf);
398} 398}
399 399
400void cgit_print_age(time_t t, time_t max_relative, char *format) 400void cgit_print_age(time_t t, time_t max_relative, char *format)
401{ 401{
402 time_t now, secs; 402 time_t now, secs;
403 403
404 if (!t) 404 if (!t)
405 return; 405 return;
406 time(&now); 406 time(&now);
407 secs = now - t; 407 secs = now - t;
408 408
409 if (secs > max_relative && max_relative >= 0) { 409 if (secs > max_relative && max_relative >= 0) {
410 cgit_print_date(t, format, ctx.cfg.local_time); 410 cgit_print_date(t, format, ctx.cfg.local_time);
411 return; 411 return;
412 } 412 }
413 413
414 if (secs < TM_HOUR * 2) { 414 if (secs < TM_HOUR * 2) {
415 htmlf("<span class='age-mins'>%.0f min.</span>", 415 htmlf("<span class='age-mins'>%.0f min.</span>",
416 secs * 1.0 / TM_MIN); 416 secs * 1.0 / TM_MIN);
417 return; 417 return;
418 } 418 }
419 if (secs < TM_DAY * 2) { 419 if (secs < TM_DAY * 2) {
420 htmlf("<span class='age-hours'>%.0f hours</span>", 420 htmlf("<span class='age-hours'>%.0f hours</span>",
421 secs * 1.0 / TM_HOUR); 421 secs * 1.0 / TM_HOUR);
422 return; 422 return;
423 } 423 }
424 if (secs < TM_WEEK * 2) { 424 if (secs < TM_WEEK * 2) {
425 htmlf("<span class='age-days'>%.0f days</span>", 425 htmlf("<span class='age-days'>%.0f days</span>",
426 secs * 1.0 / TM_DAY); 426 secs * 1.0 / TM_DAY);
427 return; 427 return;
428 } 428 }
429 if (secs < TM_MONTH * 2) { 429 if (secs < TM_MONTH * 2) {
430 htmlf("<span class='age-weeks'>%.0f weeks</span>", 430 htmlf("<span class='age-weeks'>%.0f weeks</span>",
431 secs * 1.0 / TM_WEEK); 431 secs * 1.0 / TM_WEEK);
432 return; 432 return;
433 } 433 }
434 if (secs < TM_YEAR * 2) { 434 if (secs < TM_YEAR * 2) {
435 htmlf("<span class='age-months'>%.0f months</span>", 435 htmlf("<span class='age-months'>%.0f months</span>",
436 secs * 1.0 / TM_MONTH); 436 secs * 1.0 / TM_MONTH);
437 return; 437 return;
438 } 438 }
439 htmlf("<span class='age-years'>%.0f years</span>", 439 htmlf("<span class='age-years'>%.0f years</span>",
440 secs * 1.0 / TM_YEAR); 440 secs * 1.0 / TM_YEAR);
441} 441}
442 442
443void cgit_print_http_headers(struct cgit_context *ctx) 443void cgit_print_http_headers(struct cgit_context *ctx)
444{ 444{
445 if (ctx->page.mimetype && ctx->page.charset) 445 if (ctx->page.mimetype && ctx->page.charset)
446 htmlf("Content-Type: %s; charset=%s\n", ctx->page.mimetype, 446 htmlf("Content-Type: %s; charset=%s\n", ctx->page.mimetype,
447 ctx->page.charset); 447 ctx->page.charset);
448 else if (ctx->page.mimetype) 448 else if (ctx->page.mimetype)
449 htmlf("Content-Type: %s\n", ctx->page.mimetype); 449 htmlf("Content-Type: %s\n", ctx->page.mimetype);
450 if (ctx->page.size) 450 if (ctx->page.size)
451 htmlf("Content-Length: %ld\n", ctx->page.size); 451 htmlf("Content-Length: %ld\n", ctx->page.size);
452 if (ctx->page.filename) 452 if (ctx->page.filename)
453 htmlf("Content-Disposition: inline; filename=\"%s\"\n", 453 htmlf("Content-Disposition: inline; filename=\"%s\"\n",
454 ctx->page.filename); 454 ctx->page.filename);
455 htmlf("Last-Modified: %s\n", http_date(ctx->page.modified)); 455 htmlf("Last-Modified: %s\n", http_date(ctx->page.modified));
456 htmlf("Expires: %s\n", http_date(ctx->page.expires)); 456 htmlf("Expires: %s\n", http_date(ctx->page.expires));
457 html("\n"); 457 html("\n");
458} 458}
459 459
460void cgit_print_docstart(struct cgit_context *ctx) 460void cgit_print_docstart(struct cgit_context *ctx)
461{ 461{
462 char *host = cgit_hosturl(); 462 char *host = cgit_hosturl();
463 html(cgit_doctype); 463 html(cgit_doctype);
464 html("<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n"); 464 html("<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n");
465 html("<head>\n"); 465 html("<head>\n");
466 html("<title>"); 466 html("<title>");
467 html_txt(ctx->page.title); 467 html_txt(ctx->page.title);
468 html("</title>\n"); 468 html("</title>\n");
469 htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version); 469 htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version);
470 if (ctx->cfg.robots && *ctx->cfg.robots) 470 if (ctx->cfg.robots && *ctx->cfg.robots)
471 htmlf("<meta name='robots' content='%s'/>\n", ctx->cfg.robots); 471 htmlf("<meta name='robots' content='%s'/>\n", ctx->cfg.robots);
472 html("<link rel='stylesheet' type='text/css' href='"); 472 html("<link rel='stylesheet' type='text/css' href='");
473 html_attr(ctx->cfg.css); 473 html_attr(ctx->cfg.css);
474 html("'/>\n"); 474 html("'/>\n");
475 if (ctx->cfg.favicon) { 475 if (ctx->cfg.favicon) {
476 html("<link rel='shortcut icon' href='"); 476 html("<link rel='shortcut icon' href='");
477 html_attr(ctx->cfg.favicon); 477 html_attr(ctx->cfg.favicon);
478 html("'/>\n"); 478 html("'/>\n");
479 } 479 }
480 if (host && ctx->repo) { 480 if (host && ctx->repo) {
481 html("<link rel='alternate' title='Atom feed' href='http://"); 481 html("<link rel='alternate' title='Atom feed' href='http://");
482 html_attr(cgit_hosturl()); 482 html_attr(cgit_hosturl());
483 html_attr(cgit_fileurl(ctx->repo->url, "atom", ctx->qry.path, 483 html_attr(cgit_fileurl(ctx->repo->url, "atom", ctx->qry.path,
484 fmt("h=%s", ctx->qry.head))); 484 fmt("h=%s", ctx->qry.head)));
485 html("' type='application/atom+xml'/>"); 485 html("' type='application/atom+xml'/>");
486 } 486 }
487 html("</head>\n"); 487 html("</head>\n");
488 html("<body>\n"); 488 html("<body>\n");
489} 489}
490 490
491void cgit_print_docend() 491void cgit_print_docend()
492{ 492{
493 html("</div>"); 493 html("</div>");
494 if (ctx.cfg.footer) 494 if (ctx.cfg.footer)
495 html_include(ctx.cfg.footer); 495 html_include(ctx.cfg.footer);
496 else { 496 else {
497 htmlf("<div class='footer'>generated by cgit %s at ", 497 htmlf("<div class='footer'>generated by cgit %s at ",
498 cgit_version); 498 cgit_version);
499 cgit_print_date(time(NULL), FMT_LONGDATE, ctx.cfg.local_time); 499 cgit_print_date(time(NULL), FMT_LONGDATE, ctx.cfg.local_time);
500 html("</div>\n"); 500 html("</div>\n");
501 } 501 }
502 html("</body>\n</html>\n"); 502 html("</body>\n</html>\n");
503} 503}
504 504
505int print_branch_option(const char *refname, const unsigned char *sha1, 505int print_branch_option(const char *refname, const unsigned char *sha1,
506 int flags, void *cb_data) 506 int flags, void *cb_data)
507{ 507{
508 char *name = (char *)refname; 508 char *name = (char *)refname;
509 html_option(name, name, ctx.qry.head); 509 html_option(name, name, ctx.qry.head);
510 return 0; 510 return 0;
511} 511}
512 512
513int print_archive_ref(const char *refname, const unsigned char *sha1, 513int print_archive_ref(const char *refname, const unsigned char *sha1,
514 int flags, void *cb_data) 514 int flags, void *cb_data)
515{ 515{
516 struct tag *tag; 516 struct tag *tag;
517 struct taginfo *info; 517 struct taginfo *info;
518 struct object *obj; 518 struct object *obj;
519 char buf[256], *url; 519 char buf[256], *url;
520 unsigned char fileid[20]; 520 unsigned char fileid[20];
521 int *header = (int *)cb_data; 521 int *header = (int *)cb_data;
522 522
523 if (prefixcmp(refname, "refs/archives")) 523 if (prefixcmp(refname, "refs/archives"))
524 return 0; 524 return 0;
525 strncpy(buf, refname+14, sizeof(buf)); 525 strncpy(buf, refname+14, sizeof(buf));
526 obj = parse_object(sha1); 526 obj = parse_object(sha1);
527 if (!obj) 527 if (!obj)
528 return 1; 528 return 1;
529 if (obj->type == OBJ_TAG) { 529 if (obj->type == OBJ_TAG) {
530 tag = lookup_tag(sha1); 530 tag = lookup_tag(sha1);
531 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) 531 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag)))
532 return 0; 532 return 0;
533 hashcpy(fileid, tag->tagged->sha1); 533 hashcpy(fileid, tag->tagged->sha1);
534 } else if (obj->type != OBJ_BLOB) { 534 } else if (obj->type != OBJ_BLOB) {
535 return 0; 535 return 0;
536 } else { 536 } else {
537 hashcpy(fileid, sha1); 537 hashcpy(fileid, sha1);
538 } 538 }
539 if (!*header) { 539 if (!*header) {
540 html("<h1>download</h1>\n"); 540 html("<h1>download</h1>\n");
541 *header = 1; 541 *header = 1;
542 } 542 }
543 url = cgit_pageurl(ctx.qry.repo, "blob", 543 url = cgit_pageurl(ctx.qry.repo, "blob",
544 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid), 544 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid),
545 buf)); 545 buf));
546 html_link_open(url, NULL, "menu"); 546 html_link_open(url, NULL, "menu");
547 html_txt(strlpart(buf, 20)); 547 html_txt(strlpart(buf, 20));
548 html_link_close(); 548 html_link_close();
549 return 0; 549 return 0;
550} 550}
551 551
552void add_hidden_formfields(int incl_head, int incl_search, char *page) 552void add_hidden_formfields(int incl_head, int incl_search, char *page)
553{ 553{
554 char *url; 554 char *url;
555 555
556 if (!ctx.cfg.virtual_root) { 556 if (!ctx.cfg.virtual_root) {
557 url = fmt("%s/%s", ctx.qry.repo, page); 557 url = fmt("%s/%s", ctx.qry.repo, page);
558 if (ctx.qry.path) 558 if (ctx.qry.path)
559 url = fmt("%s/%s", url, ctx.qry.path); 559 url = fmt("%s/%s", url, ctx.qry.path);
560 html_hidden("url", url); 560 html_hidden("url", url);
561 } 561 }
562 562
563 if (incl_head && ctx.qry.head && ctx.repo->defbranch && 563 if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
564 strcmp(ctx.qry.head, ctx.repo->defbranch)) 564 strcmp(ctx.qry.head, ctx.repo->defbranch))
565 html_hidden("h", ctx.qry.head); 565 html_hidden("h", ctx.qry.head);
566 566
567 if (ctx.qry.sha1) 567 if (ctx.qry.sha1)
568 html_hidden("id", ctx.qry.sha1); 568 html_hidden("id", ctx.qry.sha1);
569 if (ctx.qry.sha2) 569 if (ctx.qry.sha2)
570 html_hidden("id2", ctx.qry.sha2); 570 html_hidden("id2", ctx.qry.sha2);
571 571
572 if (incl_search) { 572 if (incl_search) {
573 if (ctx.qry.grep) 573 if (ctx.qry.grep)
574 html_hidden("qt", ctx.qry.grep); 574 html_hidden("qt", ctx.qry.grep);
575 if (ctx.qry.search) 575 if (ctx.qry.search)
576 html_hidden("q", ctx.qry.search); 576 html_hidden("q", ctx.qry.search);
577 } 577 }
578} 578}
579 579
580char *hc(struct cgit_cmd *cmd, const char *page) 580char *hc(struct cgit_cmd *cmd, const char *page)
581{ 581{
582 return (strcmp(cmd->name, page) ? NULL : "active"); 582 return (strcmp(cmd->name, page) ? NULL : "active");
583} 583}
584 584
585void cgit_print_pageheader(struct cgit_context *ctx) 585void cgit_print_pageheader(struct cgit_context *ctx)
586{ 586{
587 struct cgit_cmd *cmd = cgit_get_cmd(ctx); 587 struct cgit_cmd *cmd = cgit_get_cmd(ctx);
588 588
589 html("<table id='header'>\n"); 589 html("<table id='header'>\n");
590 html("<tr>\n"); 590 html("<tr>\n");
591 html("<td class='logo' rowspan='2'><a href='"); 591 html("<td class='logo' rowspan='2'><a href='");
592 if (ctx->cfg.logo_link) 592 if (ctx->cfg.logo_link)
593 html_attr(ctx->cfg.logo_link); 593 html_attr(ctx->cfg.logo_link);
594 else 594 else
595 html_attr(cgit_rooturl()); 595 html_attr(cgit_rooturl());
596 html("'><img src='"); 596 html("'><img src='");
597 html_attr(ctx->cfg.logo); 597 html_attr(ctx->cfg.logo);
598 html("' alt='cgit logo'/></a></td>\n"); 598 html("' alt='cgit logo'/></a></td>\n");
599 599
600 html("<td class='main'>"); 600 html("<td class='main'>");
601 if (ctx->repo) { 601 if (ctx->repo) {
602 cgit_index_link("index", NULL, NULL, NULL, 0); 602 cgit_index_link("index", NULL, NULL, NULL, 0);
603 html(" : "); 603 html(" : ");
604 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); 604 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
605 html("</td><td class='form'>"); 605 html("</td><td class='form'>");
606 html("<form method='get' action=''>\n"); 606 html("<form method='get' action=''>\n");
607 add_hidden_formfields(0, 1, ctx->qry.page); 607 add_hidden_formfields(0, 1, ctx->qry.page);
608 html("<select name='h' onchange='this.form.submit();'>\n"); 608 html("<select name='h' onchange='this.form.submit();'>\n");
609 for_each_branch_ref(print_branch_option, ctx->qry.head); 609 for_each_branch_ref(print_branch_option, ctx->qry.head);
610 html("</select> "); 610 html("</select> ");
611 html("<input type='submit' name='' value='switch'/>"); 611 html("<input type='submit' name='' value='switch'/>");
612 html("</form>"); 612 html("</form>");
613 } else 613 } else
614 html_txt(ctx->cfg.root_title); 614 html_txt(ctx->cfg.root_title);
615 html("</td></tr>\n"); 615 html("</td></tr>\n");
616 616
617 html("<tr><td class='sub'>"); 617 html("<tr><td class='sub'>");
618 if (ctx->repo) { 618 if (ctx->repo) {
619 html_txt(ctx->repo->desc); 619 html_txt(ctx->repo->desc);
620 html("</td><td class='sub right'>"); 620 html("</td><td class='sub right'>");
621 html_txt(ctx->repo->owner); 621 html_txt(ctx->repo->owner);
622 } else { 622 } else {
623 if (ctx->cfg.root_desc) 623 if (ctx->cfg.root_desc)
624 html_txt(ctx->cfg.root_desc); 624 html_txt(ctx->cfg.root_desc);
625 else if (ctx->cfg.index_info) 625 else if (ctx->cfg.index_info)
626 html_include(ctx->cfg.index_info); 626 html_include(ctx->cfg.index_info);
627 } 627 }
628 html("</td></tr></table>\n"); 628 html("</td></tr></table>\n");
629 629
630 html("<table class='tabs'><tr><td>\n"); 630 html("<table class='tabs'><tr><td>\n");
631 if (ctx->repo) { 631 if (ctx->repo) {
632 cgit_summary_link("summary", NULL, hc(cmd, "summary"), 632 cgit_summary_link("summary", NULL, hc(cmd, "summary"),
633 ctx->qry.head); 633 ctx->qry.head);
634 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head, 634 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head,
635 ctx->qry.sha1, NULL); 635 ctx->qry.sha1, NULL);
636 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head, 636 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head,
637 NULL, NULL, 0, NULL, NULL); 637 NULL, NULL, 0, NULL, NULL);
638 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 638 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
639 ctx->qry.sha1, NULL); 639 ctx->qry.sha1, NULL);
640 cgit_commit_link("commit", NULL, hc(cmd, "commit"), 640 cgit_commit_link("commit", NULL, hc(cmd, "commit"),
641 ctx->qry.head, ctx->qry.sha1); 641 ctx->qry.head, ctx->qry.sha1);
642 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 642 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
643 ctx->qry.sha1, ctx->qry.sha2, NULL); 643 ctx->qry.sha1, ctx->qry.sha2, NULL);
644 if (ctx->repo->enable_stats)
645 reporevlink("stats", "stats", NULL, hc(cmd, "stats"),
646 ctx->qry.head, NULL, NULL);
644 if (ctx->repo->readme) 647 if (ctx->repo->readme)
645 reporevlink("about", "about", NULL, 648 reporevlink("about", "about", NULL,
646 hc(cmd, "about"), ctx->qry.head, NULL, 649 hc(cmd, "about"), ctx->qry.head, NULL,
647 NULL); 650 NULL);
648 html("</td><td class='form'>"); 651 html("</td><td class='form'>");
649 html("<form class='right' method='get' action='"); 652 html("<form class='right' method='get' action='");
650 if (ctx->cfg.virtual_root) 653 if (ctx->cfg.virtual_root)
651 html_url_path(cgit_fileurl(ctx->qry.repo, "log", 654 html_url_path(cgit_fileurl(ctx->qry.repo, "log",
652 ctx->qry.path, NULL)); 655 ctx->qry.path, NULL));
653 html("'>\n"); 656 html("'>\n");
654 add_hidden_formfields(1, 0, "log"); 657 add_hidden_formfields(1, 0, "log");
655 html("<select name='qt'>\n"); 658 html("<select name='qt'>\n");
656 html_option("grep", "log msg", ctx->qry.grep); 659 html_option("grep", "log msg", ctx->qry.grep);
657 html_option("author", "author", ctx->qry.grep); 660 html_option("author", "author", ctx->qry.grep);
658 html_option("committer", "committer", ctx->qry.grep); 661 html_option("committer", "committer", ctx->qry.grep);
659 html("</select>\n"); 662 html("</select>\n");
660 html("<input class='txt' type='text' size='10' name='q' value='"); 663 html("<input class='txt' type='text' size='10' name='q' value='");
661 html_attr(ctx->qry.search); 664 html_attr(ctx->qry.search);
662 html("'/>\n"); 665 html("'/>\n");
663 html("<input type='submit' value='search'/>\n"); 666 html("<input type='submit' value='search'/>\n");
664 html("</form>\n"); 667 html("</form>\n");
665 } else { 668 } else {
666 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0); 669 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0);
667 if (ctx->cfg.root_readme) 670 if (ctx->cfg.root_readme)
668 site_link("about", "about", NULL, hc(cmd, "about"), 671 site_link("about", "about", NULL, hc(cmd, "about"),
669 NULL, 0); 672 NULL, 0);
670 html("</td><td class='form'>"); 673 html("</td><td class='form'>");
671 html("<form method='get' action='"); 674 html("<form method='get' action='");
672 html_attr(cgit_rooturl()); 675 html_attr(cgit_rooturl());
673 html("'>\n"); 676 html("'>\n");
674 html("<input type='text' name='q' size='10' value='"); 677 html("<input type='text' name='q' size='10' value='");
675 html_attr(ctx->qry.search); 678 html_attr(ctx->qry.search);
676 html("'/>\n"); 679 html("'/>\n");
677 html("<input type='submit' value='search'/>\n"); 680 html("<input type='submit' value='search'/>\n");
678 html("</form>"); 681 html("</form>");
679 } 682 }
680 html("</td></tr></table>\n"); 683 html("</td></tr></table>\n");
681 html("<div class='content'>"); 684 html("<div class='content'>");
682} 685}
683 686
684void cgit_print_filemode(unsigned short mode) 687void cgit_print_filemode(unsigned short mode)
685{ 688{
686 if (S_ISDIR(mode)) 689 if (S_ISDIR(mode))
687 html("d"); 690 html("d");
688 else if (S_ISLNK(mode)) 691 else if (S_ISLNK(mode))
689 html("l"); 692 html("l");
690 else if (S_ISGITLINK(mode)) 693 else if (S_ISGITLINK(mode))
691 html("m"); 694 html("m");
692 else 695 else
693 html("-"); 696 html("-");
694 html_fileperm(mode >> 6); 697 html_fileperm(mode >> 6);
695 html_fileperm(mode >> 3); 698 html_fileperm(mode >> 3);
696 html_fileperm(mode); 699 html_fileperm(mode);
697} 700}
698 701
699void cgit_print_snapshot_links(const char *repo, const char *head, 702void cgit_print_snapshot_links(const char *repo, const char *head,
700 const char *hex, int snapshots) 703 const char *hex, int snapshots)
701{ 704{
702 const struct cgit_snapshot_format* f; 705 const struct cgit_snapshot_format* f;
703 char *filename; 706 char *filename;
704 707
705 for (f = cgit_snapshot_formats; f->suffix; f++) { 708 for (f = cgit_snapshot_formats; f->suffix; f++) {
706 if (!(snapshots & f->bit)) 709 if (!(snapshots & f->bit))
707 continue; 710 continue;
708 filename = fmt("%s-%s%s", cgit_repobasename(repo), hex, 711 filename = fmt("%s-%s%s", cgit_repobasename(repo), hex,
709 f->suffix); 712 f->suffix);
710 cgit_snapshot_link(filename, NULL, NULL, (char *)head, 713 cgit_snapshot_link(filename, NULL, NULL, (char *)head,
711 (char *)hex, filename); 714 (char *)hex, filename);
712 html("<br/>"); 715 html("<br/>");
713 } 716 }
714} 717}
diff --git a/ui-stats.c b/ui-stats.c
new file mode 100644
index 0000000..9150840
--- a/dev/null
+++ b/ui-stats.c
@@ -0,0 +1,380 @@
1#include "cgit.h"
2#include "html.h"
3#include <string-list.h>
4
5#define MONTHS 6
6
7struct Period {
8 const char code;
9 const char *name;
10 int max_periods;
11 int count;
12
13 /* Convert a tm value to the first day in the period */
14 void (*trunc)(struct tm *tm);
15
16 /* Update tm value to start of next/previous period */
17 void (*dec)(struct tm *tm);
18 void (*inc)(struct tm *tm);
19
20 /* Pretty-print a tm value */
21 char *(*pretty)(struct tm *tm);
22};
23
24struct authorstat {
25 long total;
26 struct string_list list;
27};
28
29#define DAY_SECS (60 * 60 * 24)
30#define WEEK_SECS (DAY_SECS * 7)
31
32static void trunc_week(struct tm *tm)
33{
34 time_t t = timegm(tm);
35 t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
36 gmtime_r(&t, tm);
37}
38
39static void dec_week(struct tm *tm)
40{
41 time_t t = timegm(tm);
42 t -= WEEK_SECS;
43 gmtime_r(&t, tm);
44}
45
46static void inc_week(struct tm *tm)
47{
48 time_t t = timegm(tm);
49 t += WEEK_SECS;
50 gmtime_r(&t, tm);
51}
52
53static char *pretty_week(struct tm *tm)
54{
55 static char buf[10];
56
57 strftime(buf, sizeof(buf), "W%V %G", tm);
58 return buf;
59}
60
61static void trunc_month(struct tm *tm)
62{
63 tm->tm_mday = 1;
64}
65
66static void dec_month(struct tm *tm)
67{
68 tm->tm_mon--;
69 if (tm->tm_mon < 0) {
70 tm->tm_year--;
71 tm->tm_mon = 11;
72 }
73}
74
75static void inc_month(struct tm *tm)
76{
77 tm->tm_mon++;
78 if (tm->tm_mon > 11) {
79 tm->tm_year++;
80 tm->tm_mon = 0;
81 }
82}
83
84static char *pretty_month(struct tm *tm)
85{
86 static const char *months[] = {
87 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
88 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
89 };
90 return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
91}
92
93static void trunc_quarter(struct tm *tm)
94{
95 trunc_month(tm);
96 while(tm->tm_mon % 3 != 0)
97 dec_month(tm);
98}
99
100static void dec_quarter(struct tm *tm)
101{
102 dec_month(tm);
103 dec_month(tm);
104 dec_month(tm);
105}
106
107static void inc_quarter(struct tm *tm)
108{
109 inc_month(tm);
110 inc_month(tm);
111 inc_month(tm);
112}
113
114static char *pretty_quarter(struct tm *tm)
115{
116 return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
117}
118
119static void trunc_year(struct tm *tm)
120{
121 trunc_month(tm);
122 tm->tm_mon = 0;
123}
124
125static void dec_year(struct tm *tm)
126{
127 tm->tm_year--;
128}
129
130static void inc_year(struct tm *tm)
131{
132 tm->tm_year++;
133}
134
135static char *pretty_year(struct tm *tm)
136{
137 return fmt("%d", tm->tm_year + 1900);
138}
139
140struct Period periods[] = {
141 {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
142 {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
143 {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
144 {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
145};
146
147static void add_commit(struct string_list *authors, struct commit *commit,
148 struct Period *period)
149{
150 struct commitinfo *info;
151 struct string_list_item *author, *item;
152 struct authorstat *authorstat;
153 struct string_list *items;
154 char *tmp;
155 struct tm *date;
156 time_t t;
157
158 info = cgit_parse_commit(commit);
159 tmp = xstrdup(info->author);
160 author = string_list_insert(tmp, authors);
161 if (!author->util)
162 author->util = xcalloc(1, sizeof(struct authorstat));
163 else
164 free(tmp);
165 authorstat = author->util;
166 items = &authorstat->list;
167 t = info->committer_date;
168 date = gmtime(&t);
169 period->trunc(date);
170 tmp = xstrdup(period->pretty(date));
171 item = string_list_insert(tmp, items);
172 if (item->util)
173 free(tmp);
174 item->util++;
175 authorstat->total++;
176 cgit_free_commitinfo(info);
177}
178
179static int cmp_total_commits(const void *a1, const void *a2)
180{
181 const struct string_list_item *i1 = a1;
182 const struct string_list_item *i2 = a2;
183 const struct authorstat *auth1 = i1->util;
184 const struct authorstat *auth2 = i2->util;
185
186 return auth2->total - auth1->total;
187}
188
189/* Walk the commit DAG and collect number of commits per author per
190 * timeperiod into a nested string_list collection.
191 */
192struct string_list collect_stats(struct cgit_context *ctx,
193 struct Period *period)
194{
195 struct string_list authors;
196 struct rev_info rev;
197 struct commit *commit;
198 const char *argv[] = {NULL, ctx->qry.head, NULL, NULL};
199 time_t now;
200 long i;
201 struct tm *tm;
202 char tmp[11];
203
204 time(&now);
205 tm = gmtime(&now);
206 period->trunc(tm);
207 for (i = 1; i < period->count; i++)
208 period->dec(tm);
209 strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
210 argv[2] = xstrdup(fmt("--since=%s", tmp));
211 init_revisions(&rev, NULL);
212 rev.abbrev = DEFAULT_ABBREV;
213 rev.commit_format = CMIT_FMT_DEFAULT;
214 rev.no_merges = 1;
215 rev.verbose_header = 1;
216 rev.show_root_diff = 0;
217 setup_revisions(3, argv, &rev, NULL);
218 prepare_revision_walk(&rev);
219 memset(&authors, 0, sizeof(authors));
220 while ((commit = get_revision(&rev)) != NULL) {
221 add_commit(&authors, commit, period);
222 free(commit->buffer);
223 free_commit_list(commit->parents);
224 }
225 return authors;
226}
227
228void print_combined_authorrow(struct string_list *authors, int from, int to,
229 const char *name, const char *leftclass, const char *centerclass,
230 const char *rightclass, struct Period *period)
231{
232 struct string_list_item *author;
233 struct authorstat *authorstat;
234 struct string_list *items;
235 struct string_list_item *date;
236 time_t now;
237 long i, j, total, subtotal;
238 struct tm *tm;
239 char *tmp;
240
241 time(&now);
242 tm = gmtime(&now);
243 period->trunc(tm);
244 for (i = 1; i < period->count; i++)
245 period->dec(tm);
246
247 total = 0;
248 htmlf("<tr><td class='%s'>%s</td>", leftclass,
249 fmt(name, to - from + 1));
250 for (j = 0; j < period->count; j++) {
251 tmp = period->pretty(tm);
252 period->inc(tm);
253 subtotal = 0;
254 for (i = from; i <= to; i++) {
255 author = &authors->items[i];
256 authorstat = author->util;
257 items = &authorstat->list;
258 date = string_list_lookup(tmp, items);
259 if (date)
260 subtotal += (size_t)date->util;
261 }
262 htmlf("<td class='%s'>%d</td>", centerclass, subtotal);
263 total += subtotal;
264 }
265 htmlf("<td class='%s'>%d</td></tr>", rightclass, total);
266}
267
268void print_authors(struct string_list *authors, int top, struct Period *period)
269{
270 struct string_list_item *author;
271 struct authorstat *authorstat;
272 struct string_list *items;
273 struct string_list_item *date;
274 time_t now;
275 long i, j, total;
276 struct tm *tm;
277 char *tmp;
278
279 time(&now);
280 tm = gmtime(&now);
281 period->trunc(tm);
282 for (i = 1; i < period->count; i++)
283 period->dec(tm);
284
285 html("<table class='stats'><tr><th>Author</th>");
286 for (j = 0; j < period->count; j++) {
287 tmp = period->pretty(tm);
288 htmlf("<th>%s</th>", tmp);
289 period->inc(tm);
290 }
291 html("<th>Total</th></tr>\n");
292
293 if (top <= 0 || top > authors->nr)
294 top = authors->nr;
295
296 for (i = 0; i < top; i++) {
297 author = &authors->items[i];
298 html("<tr><td class='left'>");
299 html_txt(author->string);
300 html("</td>");
301 authorstat = author->util;
302 items = &authorstat->list;
303 total = 0;
304 for (j = 0; j < period->count; j++)
305 period->dec(tm);
306 for (j = 0; j < period->count; j++) {
307 tmp = period->pretty(tm);
308 period->inc(tm);
309 date = string_list_lookup(tmp, items);
310 if (!date)
311 html("<td>0</td>");
312 else {
313 htmlf("<td>%d</td>", date->util);
314 total += (size_t)date->util;
315 }
316 }
317 htmlf("<td class='sum'>%d</td></tr>", total);
318 }
319
320 if (top < authors->nr)
321 print_combined_authorrow(authors, top, authors->nr - 1,
322 "Others (%d)", "left", "", "sum", period);
323
324 print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
325 "total", "sum", "sum", period);
326 html("</table>");
327}
328
329/* Create a sorted string_list with one entry per author. The util-field
330 * for each author is another string_list which is used to calculate the
331 * number of commits per time-interval.
332 */
333void cgit_show_stats(struct cgit_context *ctx)
334{
335 struct string_list authors;
336 struct Period *period;
337 int top, i;
338
339 period = &periods[0];
340 if (ctx->qry.period) {
341 for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
342 if (periods[i].code == ctx->qry.period[0]) {
343 period = &periods[i];
344 break;
345 }
346 }
347 authors = collect_stats(ctx, period);
348 qsort(authors.items, authors.nr, sizeof(struct string_list_item),
349 cmp_total_commits);
350
351 top = ctx->qry.ofs;
352 if (!top)
353 top = 10;
354 htmlf("<h2>Commits per author per %s</h2>", period->name);
355
356 html("<form method='get' action='.' style='float: right; text-align: right;'>");
357 if (strcmp(ctx->qry.head, ctx->repo->defbranch))
358 htmlf("<input type='hidden' name='h' value='%s'/>", ctx->qry.head);
359 html("Period: ");
360 html("<select name='period' onchange='this.form.submit();'>");
361 for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
362 htmlf("<option value='%c'%s>%s</option>",
363 periods[i].code,
364 period == &periods[i] ? " selected" : "",
365 periods[i].name);
366 html("</select><br/><br/>");
367 html("Authors: ");
368 html("");
369 html("<select name='ofs' onchange='this.form.submit();'>");
370 htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : "");
371 htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : "");
372 htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : "");
373 htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : "");
374 htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : "");
375 html("</select>");
376 html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>");
377 html("</form>");
378 print_authors(&authors, top, period);
379}
380
diff --git a/ui-stats.h b/ui-stats.h
new file mode 100644
index 0000000..f1d744c
--- a/dev/null
+++ b/ui-stats.h
@@ -0,0 +1,8 @@
1#ifndef UI_STATS_H
2#define UI_STATS_H
3
4#include "cgit.h"
5
6extern void cgit_show_stats(struct cgit_context *ctx);
7
8#endif /* UI_STATS_H */