summaryrefslogtreecommitdiffabout
Unidiff
Diffstat (more/less context) (show whitespace changes)
-rw-r--r--Makefile20
-rw-r--r--cgit.c28
-rw-r--r--cgit.css100
-rw-r--r--cgit.h3
-rw-r--r--cgitrc.5.txt21
-rw-r--r--cmd.c7
-rw-r--r--shared.c1
-rwxr-xr-xtests/t0107-snapshot.sh22
-rw-r--r--ui-log.c38
-rw-r--r--ui-refs.c4
-rw-r--r--ui-shared.c22
-rw-r--r--ui-shared.h5
-rw-r--r--ui-snapshot.c10
-rw-r--r--ui-stats.c410
-rw-r--r--ui-stats.h27
-rw-r--r--ui-tag.c14
-rw-r--r--ui-tree.c3
17 files changed, 690 insertions, 45 deletions
diff --git a/Makefile b/Makefile
index 3c7ec07..a52285e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,32 +1,34 @@
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_DATA_PATH = $(CGIT_SCRIPT_PATH)
4CGIT_CONFIG = /etc/cgitrc 5CGIT_CONFIG = /etc/cgitrc
5CACHE_ROOT = /var/cache/cgit 6CACHE_ROOT = /var/cache/cgit
6SHA1_HEADER = <openssl/sha.h> 7SHA1_HEADER = <openssl/sha.h>
7GIT_VER = 1.6.1 8GIT_VER = 1.6.1
8GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2 9GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2
10INSTALL = install
9 11
10# Define NO_STRCASESTR if you don't have strcasestr. 12# Define NO_STRCASESTR if you don't have strcasestr.
11# 13#
12# Define NEEDS_LIBICONV if linking with libc is not enough (eg. Darwin). 14# Define NEEDS_LIBICONV if linking with libc is not enough (eg. Darwin).
13# 15#
14 16
15#-include config.mak 17#-include config.mak
16 18
17# 19#
18# Platform specific tweaks 20# Platform specific tweaks
19# 21#
20 22
21uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') 23uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
22uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not') 24uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not')
23uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not') 25uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not')
24 26
25ifeq ($(uname_O),Cygwin) 27ifeq ($(uname_O),Cygwin)
26 NO_STRCASESTR = YesPlease 28 NO_STRCASESTR = YesPlease
27 NEEDS_LIBICONV = YesPlease 29 NEEDS_LIBICONV = YesPlease
28endif 30endif
29 31
30# 32#
31# Let the user override the above settings. 33# Let the user override the above settings.
32# 34#
@@ -67,86 +69,88 @@ endif
67 69
68 70
69EXTLIBS = git/libgit.a git/xdiff/lib.a -lz -lcrypto 71EXTLIBS = git/libgit.a git/xdiff/lib.a -lz -lcrypto
70OBJECTS = 72OBJECTS =
71OBJECTS += cache.o 73OBJECTS += cache.o
72OBJECTS += cgit.o 74OBJECTS += cgit.o
73OBJECTS += cmd.o 75OBJECTS += cmd.o
74OBJECTS += configfile.o 76OBJECTS += configfile.o
75OBJECTS += html.o 77OBJECTS += html.o
76OBJECTS += parsing.o 78OBJECTS += parsing.o
77OBJECTS += scan-tree.o 79OBJECTS += scan-tree.o
78OBJECTS += shared.o 80OBJECTS += shared.o
79OBJECTS += ui-atom.o 81OBJECTS += ui-atom.o
80OBJECTS += ui-blob.o 82OBJECTS += ui-blob.o
81OBJECTS += ui-clone.o 83OBJECTS += ui-clone.o
82OBJECTS += ui-commit.o 84OBJECTS += ui-commit.o
83OBJECTS += ui-diff.o 85OBJECTS += ui-diff.o
84OBJECTS += ui-log.o 86OBJECTS += ui-log.o
85OBJECTS += ui-patch.o 87OBJECTS += ui-patch.o
86OBJECTS += ui-plain.o 88OBJECTS += ui-plain.o
87OBJECTS += ui-refs.o 89OBJECTS += ui-refs.o
88OBJECTS += ui-repolist.o 90OBJECTS += ui-repolist.o
89OBJECTS += ui-shared.o 91OBJECTS += ui-shared.o
90OBJECTS += ui-snapshot.o 92OBJECTS += ui-snapshot.o
93OBJECTS += ui-stats.o
91OBJECTS += ui-summary.o 94OBJECTS += ui-summary.o
92OBJECTS += ui-tag.o 95OBJECTS += ui-tag.o
93OBJECTS += ui-tree.o 96OBJECTS += ui-tree.o
94 97
95ifdef NEEDS_LIBICONV 98ifdef NEEDS_LIBICONV
96 EXTLIBS += -liconv 99 EXTLIBS += -liconv
97endif 100endif
98 101
99 102
100.PHONY: all libgit test install uninstall clean force-version get-git 103.PHONY: all libgit test install uninstall clean force-version get-git
101 104
102all: cgit 105all: cgit
103 106
104VERSION: force-version 107VERSION: force-version
105 @./gen-version.sh "$(CGIT_VERSION)" 108 @./gen-version.sh "$(CGIT_VERSION)"
106-include VERSION 109-include VERSION
107 110
108 111
109CFLAGS += -g -Wall -Igit 112CFLAGS += -g -Wall -Igit
110CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)' 113CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)'
111CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"' 114CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"'
112CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"' 115CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"'
113CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"' 116CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"'
114CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"' 117CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"'
115 118
116ifdef NO_ICONV 119ifdef NO_ICONV
117 CFLAGS += -DNO_ICONV 120 CFLAGS += -DNO_ICONV
118endif 121endif
119ifdef NO_STRCASESTR 122ifdef NO_STRCASESTR
120 CFLAGS += -DNO_STRCASESTR 123 CFLAGS += -DNO_STRCASESTR
121endif 124endif
122 125
123cgit: $(OBJECTS) libgit 126cgit: $(OBJECTS) libgit
124 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS) 127 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS)
125 128
126cgit.o: VERSION 129cgit.o: VERSION
127 130
128-include $(OBJECTS:.o=.d) 131-include $(OBJECTS:.o=.d)
129 132
130libgit: 133libgit:
131 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) libgit.a 134 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 libgit.a
132 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) xdiff/lib.a 135 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 xdiff/lib.a
133 136
134test: all 137test: all
135 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all 138 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all
136 139
137install: all 140install: all
138 mkdir -p $(DESTDIR)$(CGIT_SCRIPT_PATH) 141 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH)
139 install cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 142 $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
140 install -m 0644 cgit.css $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.css 143 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH)
141 install -m 0644 cgit.png $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.png 144 $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css
145 $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png
142 146
143uninstall: 147uninstall:
144 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 148 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
145 rm -f $(CGIT_SCRIPT_PATH)/cgit.css 149 rm -f $(CGIT_DATA_PATH)/cgit.css
146 rm -f $(CGIT_SCRIPT_PATH)/cgit.png 150 rm -f $(CGIT_DATA_PATH)/cgit.png
147 151
148clean: 152clean:
149 rm -f cgit VERSION *.o *.d 153 rm -f cgit VERSION *.o *.d
150 154
151get-git: 155get-git:
152 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git 156 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git
diff --git a/cgit.c b/cgit.c
index 166fbc6..608cab6 100644
--- a/cgit.c
+++ b/cgit.c
@@ -1,80 +1,83 @@
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 "ui-stats.h"
15#include "scan-tree.h" 16#include "scan-tree.h"
16 17
17const char *cgit_version = CGIT_VERSION; 18const char *cgit_version = CGIT_VERSION;
18 19
19void config_cb(const char *name, const char *value) 20void config_cb(const char *name, const char *value)
20{ 21{
21 if (!strcmp(name, "root-title")) 22 if (!strcmp(name, "root-title"))
22 ctx.cfg.root_title = xstrdup(value); 23 ctx.cfg.root_title = xstrdup(value);
23 else if (!strcmp(name, "root-desc")) 24 else if (!strcmp(name, "root-desc"))
24 ctx.cfg.root_desc = xstrdup(value); 25 ctx.cfg.root_desc = xstrdup(value);
25 else if (!strcmp(name, "root-readme")) 26 else if (!strcmp(name, "root-readme"))
26 ctx.cfg.root_readme = xstrdup(value); 27 ctx.cfg.root_readme = xstrdup(value);
27 else if (!strcmp(name, "css")) 28 else if (!strcmp(name, "css"))
28 ctx.cfg.css = xstrdup(value); 29 ctx.cfg.css = xstrdup(value);
29 else if (!strcmp(name, "favicon")) 30 else if (!strcmp(name, "favicon"))
30 ctx.cfg.favicon = xstrdup(value); 31 ctx.cfg.favicon = xstrdup(value);
31 else if (!strcmp(name, "footer")) 32 else if (!strcmp(name, "footer"))
32 ctx.cfg.footer = xstrdup(value); 33 ctx.cfg.footer = xstrdup(value);
33 else if (!strcmp(name, "logo")) 34 else if (!strcmp(name, "logo"))
34 ctx.cfg.logo = xstrdup(value); 35 ctx.cfg.logo = xstrdup(value);
35 else if (!strcmp(name, "index-header")) 36 else if (!strcmp(name, "index-header"))
36 ctx.cfg.index_header = xstrdup(value); 37 ctx.cfg.index_header = xstrdup(value);
37 else if (!strcmp(name, "index-info")) 38 else if (!strcmp(name, "index-info"))
38 ctx.cfg.index_info = xstrdup(value); 39 ctx.cfg.index_info = xstrdup(value);
39 else if (!strcmp(name, "logo-link")) 40 else if (!strcmp(name, "logo-link"))
40 ctx.cfg.logo_link = xstrdup(value); 41 ctx.cfg.logo_link = xstrdup(value);
41 else if (!strcmp(name, "module-link")) 42 else if (!strcmp(name, "module-link"))
42 ctx.cfg.module_link = xstrdup(value); 43 ctx.cfg.module_link = xstrdup(value);
43 else if (!strcmp(name, "virtual-root")) { 44 else if (!strcmp(name, "virtual-root")) {
44 ctx.cfg.virtual_root = trim_end(value, '/'); 45 ctx.cfg.virtual_root = trim_end(value, '/');
45 if (!ctx.cfg.virtual_root && (!strcmp(value, "/"))) 46 if (!ctx.cfg.virtual_root && (!strcmp(value, "/")))
46 ctx.cfg.virtual_root = ""; 47 ctx.cfg.virtual_root = "";
47 } else if (!strcmp(name, "nocache")) 48 } else if (!strcmp(name, "nocache"))
48 ctx.cfg.nocache = atoi(value); 49 ctx.cfg.nocache = atoi(value);
49 else if (!strcmp(name, "snapshots")) 50 else if (!strcmp(name, "snapshots"))
50 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); 51 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
51 else if (!strcmp(name, "enable-index-links")) 52 else if (!strcmp(name, "enable-index-links"))
52 ctx.cfg.enable_index_links = atoi(value); 53 ctx.cfg.enable_index_links = atoi(value);
53 else if (!strcmp(name, "enable-log-filecount")) 54 else if (!strcmp(name, "enable-log-filecount"))
54 ctx.cfg.enable_log_filecount = atoi(value); 55 ctx.cfg.enable_log_filecount = atoi(value);
55 else if (!strcmp(name, "enable-log-linecount")) 56 else if (!strcmp(name, "enable-log-linecount"))
56 ctx.cfg.enable_log_linecount = atoi(value); 57 ctx.cfg.enable_log_linecount = atoi(value);
58 else if (!strcmp(name, "max-stats"))
59 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
57 else if (!strcmp(name, "cache-size")) 60 else if (!strcmp(name, "cache-size"))
58 ctx.cfg.cache_size = atoi(value); 61 ctx.cfg.cache_size = atoi(value);
59 else if (!strcmp(name, "cache-root")) 62 else if (!strcmp(name, "cache-root"))
60 ctx.cfg.cache_root = xstrdup(value); 63 ctx.cfg.cache_root = xstrdup(value);
61 else if (!strcmp(name, "cache-root-ttl")) 64 else if (!strcmp(name, "cache-root-ttl"))
62 ctx.cfg.cache_root_ttl = atoi(value); 65 ctx.cfg.cache_root_ttl = atoi(value);
63 else if (!strcmp(name, "cache-repo-ttl")) 66 else if (!strcmp(name, "cache-repo-ttl"))
64 ctx.cfg.cache_repo_ttl = atoi(value); 67 ctx.cfg.cache_repo_ttl = atoi(value);
65 else if (!strcmp(name, "cache-static-ttl")) 68 else if (!strcmp(name, "cache-static-ttl"))
66 ctx.cfg.cache_static_ttl = atoi(value); 69 ctx.cfg.cache_static_ttl = atoi(value);
67 else if (!strcmp(name, "cache-dynamic-ttl")) 70 else if (!strcmp(name, "cache-dynamic-ttl"))
68 ctx.cfg.cache_dynamic_ttl = atoi(value); 71 ctx.cfg.cache_dynamic_ttl = atoi(value);
69 else if (!strcmp(name, "max-message-length")) 72 else if (!strcmp(name, "max-message-length"))
70 ctx.cfg.max_msg_len = atoi(value); 73 ctx.cfg.max_msg_len = atoi(value);
71 else if (!strcmp(name, "max-repodesc-length")) 74 else if (!strcmp(name, "max-repodesc-length"))
72 ctx.cfg.max_repodesc_len = atoi(value); 75 ctx.cfg.max_repodesc_len = atoi(value);
73 else if (!strcmp(name, "max-repo-count")) 76 else if (!strcmp(name, "max-repo-count"))
74 ctx.cfg.max_repo_count = atoi(value); 77 ctx.cfg.max_repo_count = atoi(value);
75 else if (!strcmp(name, "max-commit-count")) 78 else if (!strcmp(name, "max-commit-count"))
76 ctx.cfg.max_commit_count = atoi(value); 79 ctx.cfg.max_commit_count = atoi(value);
77 else if (!strcmp(name, "summary-log")) 80 else if (!strcmp(name, "summary-log"))
78 ctx.cfg.summary_log = atoi(value); 81 ctx.cfg.summary_log = atoi(value);
79 else if (!strcmp(name, "summary-branches")) 82 else if (!strcmp(name, "summary-branches"))
80 ctx.cfg.summary_branches = atoi(value); 83 ctx.cfg.summary_branches = atoi(value);
@@ -91,117 +94,122 @@ void config_cb(const char *name, const char *value)
91 else if (!strcmp(name, "local-time")) 94 else if (!strcmp(name, "local-time"))
92 ctx.cfg.local_time = atoi(value); 95 ctx.cfg.local_time = atoi(value);
93 else if (!strcmp(name, "repo.group")) 96 else if (!strcmp(name, "repo.group"))
94 ctx.cfg.repo_group = xstrdup(value); 97 ctx.cfg.repo_group = xstrdup(value);
95 else if (!strcmp(name, "repo.url")) 98 else if (!strcmp(name, "repo.url"))
96 ctx.repo = cgit_add_repo(value); 99 ctx.repo = cgit_add_repo(value);
97 else if (!strcmp(name, "repo.name")) 100 else if (!strcmp(name, "repo.name"))
98 ctx.repo->name = xstrdup(value); 101 ctx.repo->name = xstrdup(value);
99 else if (ctx.repo && !strcmp(name, "repo.path")) 102 else if (ctx.repo && !strcmp(name, "repo.path"))
100 ctx.repo->path = trim_end(value, '/'); 103 ctx.repo->path = trim_end(value, '/');
101 else if (ctx.repo && !strcmp(name, "repo.clone-url")) 104 else if (ctx.repo && !strcmp(name, "repo.clone-url"))
102 ctx.repo->clone_url = xstrdup(value); 105 ctx.repo->clone_url = xstrdup(value);
103 else if (ctx.repo && !strcmp(name, "repo.desc")) 106 else if (ctx.repo && !strcmp(name, "repo.desc"))
104 ctx.repo->desc = xstrdup(value); 107 ctx.repo->desc = xstrdup(value);
105 else if (ctx.repo && !strcmp(name, "repo.owner")) 108 else if (ctx.repo && !strcmp(name, "repo.owner"))
106 ctx.repo->owner = xstrdup(value); 109 ctx.repo->owner = xstrdup(value);
107 else if (ctx.repo && !strcmp(name, "repo.defbranch")) 110 else if (ctx.repo && !strcmp(name, "repo.defbranch"))
108 ctx.repo->defbranch = xstrdup(value); 111 ctx.repo->defbranch = xstrdup(value);
109 else if (ctx.repo && !strcmp(name, "repo.snapshots")) 112 else if (ctx.repo && !strcmp(name, "repo.snapshots"))
110 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */ 113 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */
111 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount")) 114 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount"))
112 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value); 115 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
113 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount")) 116 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
114 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value); 117 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
118 else if (ctx.repo && !strcmp(name, "repo.max-stats"))
119 ctx.repo->max_stats = cgit_find_stats_period(value, NULL);
115 else if (ctx.repo && !strcmp(name, "repo.module-link")) 120 else if (ctx.repo && !strcmp(name, "repo.module-link"))
116 ctx.repo->module_link= xstrdup(value); 121 ctx.repo->module_link= xstrdup(value);
117 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) { 122 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
118 if (*value == '/') 123 if (*value == '/')
119 ctx.repo->readme = xstrdup(value); 124 ctx.repo->readme = xstrdup(value);
120 else 125 else
121 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value)); 126 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
122 } else if (!strcmp(name, "include")) 127 } else if (!strcmp(name, "include"))
123 parse_configfile(value, config_cb); 128 parse_configfile(value, config_cb);
124} 129}
125 130
126static void querystring_cb(const char *name, const char *value) 131static void querystring_cb(const char *name, const char *value)
127{ 132{
128 if (!strcmp(name,"r")) { 133 if (!strcmp(name,"r")) {
129 ctx.qry.repo = xstrdup(value); 134 ctx.qry.repo = xstrdup(value);
130 ctx.repo = cgit_get_repoinfo(value); 135 ctx.repo = cgit_get_repoinfo(value);
131 } else if (!strcmp(name, "p")) { 136 } else if (!strcmp(name, "p")) {
132 ctx.qry.page = xstrdup(value); 137 ctx.qry.page = xstrdup(value);
133 } else if (!strcmp(name, "url")) { 138 } else if (!strcmp(name, "url")) {
134 ctx.qry.url = xstrdup(value); 139 ctx.qry.url = xstrdup(value);
135 cgit_parse_url(value); 140 cgit_parse_url(value);
136 } else if (!strcmp(name, "qt")) { 141 } else if (!strcmp(name, "qt")) {
137 ctx.qry.grep = xstrdup(value); 142 ctx.qry.grep = xstrdup(value);
138 } else if (!strcmp(name, "q")) { 143 } else if (!strcmp(name, "q")) {
139 ctx.qry.search = xstrdup(value); 144 ctx.qry.search = xstrdup(value);
140 } else if (!strcmp(name, "h")) { 145 } else if (!strcmp(name, "h")) {
141 ctx.qry.head = xstrdup(value); 146 ctx.qry.head = xstrdup(value);
142 ctx.qry.has_symref = 1; 147 ctx.qry.has_symref = 1;
143 } else if (!strcmp(name, "id")) { 148 } else if (!strcmp(name, "id")) {
144 ctx.qry.sha1 = xstrdup(value); 149 ctx.qry.sha1 = xstrdup(value);
145 ctx.qry.has_sha1 = 1; 150 ctx.qry.has_sha1 = 1;
146 } else if (!strcmp(name, "id2")) { 151 } else if (!strcmp(name, "id2")) {
147 ctx.qry.sha2 = xstrdup(value); 152 ctx.qry.sha2 = xstrdup(value);
148 ctx.qry.has_sha1 = 1; 153 ctx.qry.has_sha1 = 1;
149 } else if (!strcmp(name, "ofs")) { 154 } else if (!strcmp(name, "ofs")) {
150 ctx.qry.ofs = atoi(value); 155 ctx.qry.ofs = atoi(value);
151 } else if (!strcmp(name, "path")) { 156 } else if (!strcmp(name, "path")) {
152 ctx.qry.path = trim_end(value, '/'); 157 ctx.qry.path = trim_end(value, '/');
153 } else if (!strcmp(name, "name")) { 158 } else if (!strcmp(name, "name")) {
154 ctx.qry.name = xstrdup(value); 159 ctx.qry.name = xstrdup(value);
155 } else if (!strcmp(name, "mimetype")) { 160 } else if (!strcmp(name, "mimetype")) {
156 ctx.qry.mimetype = xstrdup(value); 161 ctx.qry.mimetype = xstrdup(value);
157 } else if (!strcmp(name, "s")){ 162 } else if (!strcmp(name, "s")){
158 ctx.qry.sort = xstrdup(value); 163 ctx.qry.sort = xstrdup(value);
159 } else if (!strcmp(name, "showmsg")) { 164 } else if (!strcmp(name, "showmsg")) {
160 ctx.qry.showmsg = atoi(value); 165 ctx.qry.showmsg = atoi(value);
166 } else if (!strcmp(name, "period")) {
167 ctx.qry.period = xstrdup(value);
161 } 168 }
162} 169}
163 170
164static void prepare_context(struct cgit_context *ctx) 171static void prepare_context(struct cgit_context *ctx)
165{ 172{
166 memset(ctx, 0, sizeof(ctx)); 173 memset(ctx, 0, sizeof(ctx));
167 ctx->cfg.agefile = "info/web/last-modified"; 174 ctx->cfg.agefile = "info/web/last-modified";
168 ctx->cfg.nocache = 0; 175 ctx->cfg.nocache = 0;
169 ctx->cfg.cache_size = 0; 176 ctx->cfg.cache_size = 0;
170 ctx->cfg.cache_dynamic_ttl = 5; 177 ctx->cfg.cache_dynamic_ttl = 5;
171 ctx->cfg.cache_max_create_time = 5; 178 ctx->cfg.cache_max_create_time = 5;
172 ctx->cfg.cache_repo_ttl = 5; 179 ctx->cfg.cache_repo_ttl = 5;
173 ctx->cfg.cache_root = CGIT_CACHE_ROOT; 180 ctx->cfg.cache_root = CGIT_CACHE_ROOT;
174 ctx->cfg.cache_root_ttl = 5; 181 ctx->cfg.cache_root_ttl = 5;
175 ctx->cfg.cache_static_ttl = -1; 182 ctx->cfg.cache_static_ttl = -1;
176 ctx->cfg.css = "/cgit.css"; 183 ctx->cfg.css = "/cgit.css";
177 ctx->cfg.logo = "/git-logo.png"; 184 ctx->cfg.logo = "/git-logo.png";
178 ctx->cfg.local_time = 0; 185 ctx->cfg.local_time = 0;
179 ctx->cfg.max_repo_count = 50; 186 ctx->cfg.max_repo_count = 50;
180 ctx->cfg.max_commit_count = 50; 187 ctx->cfg.max_commit_count = 50;
181 ctx->cfg.max_lock_attempts = 5; 188 ctx->cfg.max_lock_attempts = 5;
182 ctx->cfg.max_msg_len = 80; 189 ctx->cfg.max_msg_len = 80;
183 ctx->cfg.max_repodesc_len = 80; 190 ctx->cfg.max_repodesc_len = 80;
191 ctx->cfg.max_stats = 0;
184 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; 192 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s";
185 ctx->cfg.renamelimit = -1; 193 ctx->cfg.renamelimit = -1;
186 ctx->cfg.robots = "index, nofollow"; 194 ctx->cfg.robots = "index, nofollow";
187 ctx->cfg.root_title = "Git repository browser"; 195 ctx->cfg.root_title = "Git repository browser";
188 ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 196 ctx->cfg.root_desc = "a fast webinterface for the git dscm";
189 ctx->cfg.script_name = CGIT_SCRIPT_NAME; 197 ctx->cfg.script_name = CGIT_SCRIPT_NAME;
190 ctx->cfg.summary_branches = 10; 198 ctx->cfg.summary_branches = 10;
191 ctx->cfg.summary_log = 10; 199 ctx->cfg.summary_log = 10;
192 ctx->cfg.summary_tags = 10; 200 ctx->cfg.summary_tags = 10;
193 ctx->page.mimetype = "text/html"; 201 ctx->page.mimetype = "text/html";
194 ctx->page.charset = PAGE_ENCODING; 202 ctx->page.charset = PAGE_ENCODING;
195 ctx->page.filename = NULL; 203 ctx->page.filename = NULL;
196 ctx->page.size = 0; 204 ctx->page.size = 0;
197 ctx->page.modified = time(NULL); 205 ctx->page.modified = time(NULL);
198 ctx->page.expires = ctx->page.modified; 206 ctx->page.expires = ctx->page.modified;
199} 207}
200 208
201struct refmatch { 209struct refmatch {
202 char *req_ref; 210 char *req_ref;
203 char *first_ref; 211 char *first_ref;
204 int match; 212 int match;
205}; 213};
206 214
207int find_current_ref(const char *refname, const unsigned char *sha1, 215int find_current_ref(const char *refname, const unsigned char *sha1,
@@ -272,49 +280,48 @@ static int prepare_repo_cmd(struct cgit_context *ctx)
272 return 1; 280 return 1;
273 } 281 }
274 282
275 if (get_sha1(ctx->qry.head, sha1)) { 283 if (get_sha1(ctx->qry.head, sha1)) {
276 tmp = xstrdup(ctx->qry.head); 284 tmp = xstrdup(ctx->qry.head);
277 ctx->qry.head = ctx->repo->defbranch; 285 ctx->qry.head = ctx->repo->defbranch;
278 cgit_print_http_headers(ctx); 286 cgit_print_http_headers(ctx);
279 cgit_print_docstart(ctx); 287 cgit_print_docstart(ctx);
280 cgit_print_pageheader(ctx); 288 cgit_print_pageheader(ctx);
281 cgit_print_error(fmt("Invalid branch: %s", tmp)); 289 cgit_print_error(fmt("Invalid branch: %s", tmp));
282 cgit_print_docend(); 290 cgit_print_docend();
283 return 1; 291 return 1;
284 } 292 }
285 return 0; 293 return 0;
286} 294}
287 295
288static void process_request(void *cbdata) 296static void process_request(void *cbdata)
289{ 297{
290 struct cgit_context *ctx = cbdata; 298 struct cgit_context *ctx = cbdata;
291 struct cgit_cmd *cmd; 299 struct cgit_cmd *cmd;
292 300
293 cmd = cgit_get_cmd(ctx); 301 cmd = cgit_get_cmd(ctx);
294 if (!cmd) { 302 if (!cmd) {
295 ctx->page.title = "cgit error"; 303 ctx->page.title = "cgit error";
296 ctx->repo = NULL;
297 cgit_print_http_headers(ctx); 304 cgit_print_http_headers(ctx);
298 cgit_print_docstart(ctx); 305 cgit_print_docstart(ctx);
299 cgit_print_pageheader(ctx); 306 cgit_print_pageheader(ctx);
300 cgit_print_error("Invalid request"); 307 cgit_print_error("Invalid request");
301 cgit_print_docend(); 308 cgit_print_docend();
302 return; 309 return;
303 } 310 }
304 311
305 if (cmd->want_repo && !ctx->repo) { 312 if (cmd->want_repo && !ctx->repo) {
306 cgit_print_http_headers(ctx); 313 cgit_print_http_headers(ctx);
307 cgit_print_docstart(ctx); 314 cgit_print_docstart(ctx);
308 cgit_print_pageheader(ctx); 315 cgit_print_pageheader(ctx);
309 cgit_print_error(fmt("No repository selected")); 316 cgit_print_error(fmt("No repository selected"));
310 cgit_print_docend(); 317 cgit_print_docend();
311 return; 318 return;
312 } 319 }
313 320
314 if (ctx->repo && prepare_repo_cmd(ctx)) 321 if (ctx->repo && prepare_repo_cmd(ctx))
315 return; 322 return;
316 323
317 if (cmd->want_layout) { 324 if (cmd->want_layout) {
318 cgit_print_http_headers(ctx); 325 cgit_print_http_headers(ctx);
319 cgit_print_docstart(ctx); 326 cgit_print_docstart(ctx);
320 cgit_print_pageheader(ctx); 327 cgit_print_pageheader(ctx);
@@ -418,59 +425,60 @@ static int calc_ttl()
418} 425}
419 426
420int main(int argc, const char **argv) 427int main(int argc, const char **argv)
421{ 428{
422 const char *cgit_config_env = getenv("CGIT_CONFIG"); 429 const char *cgit_config_env = getenv("CGIT_CONFIG");
423 const char *path; 430 const char *path;
424 char *qry; 431 char *qry;
425 int err, ttl; 432 int err, ttl;
426 433
427 prepare_context(&ctx); 434 prepare_context(&ctx);
428 cgit_repolist.length = 0; 435 cgit_repolist.length = 0;
429 cgit_repolist.count = 0; 436 cgit_repolist.count = 0;
430 cgit_repolist.repos = NULL; 437 cgit_repolist.repos = NULL;
431 438
432 if (getenv("SCRIPT_NAME")) 439 if (getenv("SCRIPT_NAME"))
433 ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME")); 440 ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME"));
434 if (getenv("QUERY_STRING")) 441 if (getenv("QUERY_STRING"))
435 ctx.qry.raw = xstrdup(getenv("QUERY_STRING")); 442 ctx.qry.raw = xstrdup(getenv("QUERY_STRING"));
436 cgit_parse_args(argc, argv); 443 cgit_parse_args(argc, argv);
437 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG, 444 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG,
438 config_cb); 445 config_cb);
439 ctx.repo = NULL; 446 ctx.repo = NULL;
440 http_parse_querystring(ctx.qry.raw, querystring_cb); 447 http_parse_querystring(ctx.qry.raw, querystring_cb);
441 448
442 /* If virtual-root isn't specified in cgitrc and no url 449 /* If virtual-root isn't specified in cgitrc, lets pretend
443 * parameter is specified on the querystring, lets pretend 450 * that virtual-root equals SCRIPT_NAME.
444 * that virtualroot equals SCRIPT_NAME and use PATH_INFO as
445 * url. This allows cgit to work with virtual urls without
446 * the need for rewriterules in the webserver (as long as
447 * PATH_INFO is included in the cache lookup key).
448 */ 451 */
449 if (!ctx.cfg.virtual_root && !ctx.qry.url) { 452 if (!ctx.cfg.virtual_root)
450 ctx.cfg.virtual_root = ctx.cfg.script_name; 453 ctx.cfg.virtual_root = ctx.cfg.script_name;
454
455 /* If no url parameter is specified on the querystring, lets
456 * use PATH_INFO as url. This allows cgit to work with virtual
457 * urls without the need for rewriterules in the webserver (as
458 * long as PATH_INFO is included in the cache lookup key).
459 */
451 path = getenv("PATH_INFO"); 460 path = getenv("PATH_INFO");
452 if (path) { 461 if (!ctx.qry.url && path) {
453 if (path[0] == '/') 462 if (path[0] == '/')
454 path++; 463 path++;
455 ctx.qry.url = xstrdup(path); 464 ctx.qry.url = xstrdup(path);
456 if (ctx.qry.raw) { 465 if (ctx.qry.raw) {
457 qry = ctx.qry.raw; 466 qry = ctx.qry.raw;
458 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 467 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
459 free(qry); 468 free(qry);
460 } else 469 } else
461 ctx.qry.raw = ctx.qry.url; 470 ctx.qry.raw = ctx.qry.url;
462 cgit_parse_url(ctx.qry.url); 471 cgit_parse_url(ctx.qry.url);
463 } 472 }
464 }
465 473
466 ttl = calc_ttl(); 474 ttl = calc_ttl();
467 ctx.page.expires += ttl*60; 475 ctx.page.expires += ttl*60;
468 if (ctx.cfg.nocache) 476 if (ctx.cfg.nocache)
469 ctx.cfg.cache_size = 0; 477 ctx.cfg.cache_size = 0;
470 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 478 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
471 ctx.qry.raw, ttl, process_request, &ctx); 479 ctx.qry.raw, ttl, process_request, &ctx);
472 if (err) 480 if (err)
473 cgit_print_error(fmt("Error processing page: %s (%d)", 481 cgit_print_error(fmt("Error processing page: %s (%d)",
474 strerror(err), err)); 482 strerror(err), err));
475 return err; 483 return err;
476} 484}
diff --git a/cgit.css b/cgit.css
index 068c37b..f844efa 100644
--- a/cgit.css
+++ b/cgit.css
@@ -455,24 +455,124 @@ span.age-hours {
455 color: #080; 455 color: #080;
456} 456}
457 457
458span.age-days { 458span.age-days {
459 color: #040; 459 color: #040;
460} 460}
461 461
462span.age-weeks { 462span.age-weeks {
463 color: #444; 463 color: #444;
464} 464}
465 465
466span.age-months { 466span.age-months {
467 color: #888; 467 color: #888;
468} 468}
469 469
470span.age-years { 470span.age-years {
471 color: #bbb; 471 color: #bbb;
472} 472}
473div.footer { 473div.footer {
474 margin-top: 0.5em; 474 margin-top: 0.5em;
475 text-align: center; 475 text-align: center;
476 font-size: 80%; 476 font-size: 80%;
477 color: #ccc; 477 color: #ccc;
478} 478}
479a.branch-deco {
480 margin: 0px 0.5em;
481 padding: 0px 0.25em;
482 background-color: #88ff88;
483 border: solid 1px #007700;
484}
485a.tag-deco {
486 margin: 0px 0.5em;
487 padding: 0px 0.25em;
488 background-color: #ffff88;
489 border: solid 1px #777700;
490}
491a.remote-deco {
492 margin: 0px 0.5em;
493 padding: 0px 0.25em;
494 background-color: #ccccff;
495 border: solid 1px #000077;
496}
497a.deco {
498 margin: 0px 0.5em;
499 padding: 0px 0.25em;
500 background-color: #ff8888;
501 border: solid 1px #770000;
502}
503table.stats {
504 border: solid 1px black;
505 border-collapse: collapse;
506}
507
508table.stats th {
509 text-align: left;
510 padding: 1px 0.5em;
511 background-color: #eee;
512 border: solid 1px black;
513}
514
515table.stats td {
516 text-align: right;
517 padding: 1px 0.5em;
518 border: solid 1px black;
519}
520
521table.stats td.total {
522 font-weight: bold;
523 text-align: left;
524}
525
526table.stats td.sum {
527 color: #c00;
528 font-weight: bold;
529 /*background-color: #eee; */
530}
531
532table.stats td.left {
533 text-align: left;
534}
535
536table.vgraph {
537 border-collapse: separate;
538 border: solid 1px black;
539 height: 200px;
540}
541
542table.vgraph th {
543 background-color: #eee;
544 font-weight: bold;
545 border: solid 1px white;
546 padding: 1px 0.5em;
547}
548
549table.vgraph td {
550 vertical-align: bottom;
551 padding: 0px 10px;
552}
553
554table.vgraph div.bar {
555 background-color: #eee;
556}
557
558table.hgraph {
559 border: solid 1px black;
560 width: 800px;
561}
562
563table.hgraph th {
564 background-color: #eee;
565 font-weight: bold;
566 border: solid 1px black;
567 padding: 1px 0.5em;
568}
569
570table.hgraph td {
571 vertical-align: center;
572 padding: 2px 2px;
573}
574
575table.hgraph div.bar {
576 background-color: #eee;
577 height: 1em;
578}
diff --git a/cgit.h b/cgit.h
index cb2f176..4fe94c6 100644
--- a/cgit.h
+++ b/cgit.h
@@ -40,48 +40,49 @@
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 max_stats;
64 time_t mtime; 65 time_t mtime;
65}; 66};
66 67
67struct cgit_repolist { 68struct cgit_repolist {
68 int length; 69 int length;
69 int count; 70 int count;
70 struct cgit_repo *repos; 71 struct cgit_repo *repos;
71}; 72};
72 73
73struct commitinfo { 74struct commitinfo {
74 struct commit *commit; 75 struct commit *commit;
75 char *author; 76 char *author;
76 char *author_email; 77 char *author_email;
77 unsigned long author_date; 78 unsigned long author_date;
78 char *committer; 79 char *committer;
79 char *committer_email; 80 char *committer_email;
80 unsigned long committer_date; 81 unsigned long committer_date;
81 char *subject; 82 char *subject;
82 char *msg; 83 char *msg;
83 char *msg_encoding; 84 char *msg_encoding;
84}; 85};
85 86
86struct taginfo { 87struct taginfo {
87 char *tagger; 88 char *tagger;
@@ -99,88 +100,90 @@ struct refinfo {
99 }; 100 };
100}; 101};
101 102
102struct reflist { 103struct reflist {
103 struct refinfo **refs; 104 struct refinfo **refs;
104 int alloc; 105 int alloc;
105 int count; 106 int count;
106}; 107};
107 108
108struct cgit_query { 109struct cgit_query {
109 int has_symref; 110 int has_symref;
110 int has_sha1; 111 int has_sha1;
111 char *raw; 112 char *raw;
112 char *repo; 113 char *repo;
113 char *page; 114 char *page;
114 char *search; 115 char *search;
115 char *grep; 116 char *grep;
116 char *head; 117 char *head;
117 char *sha1; 118 char *sha1;
118 char *sha2; 119 char *sha2;
119 char *path; 120 char *path;
120 char *name; 121 char *name;
121 char *mimetype; 122 char *mimetype;
122 char *url; 123 char *url;
124 char *period;
123 int ofs; 125 int ofs;
124 int nohead; 126 int nohead;
125 char *sort; 127 char *sort;
126 int showmsg; 128 int showmsg;
127}; 129};
128 130
129struct cgit_config { 131struct cgit_config {
130 char *agefile; 132 char *agefile;
131 char *cache_root; 133 char *cache_root;
132 char *clone_prefix; 134 char *clone_prefix;
133 char *css; 135 char *css;
134 char *favicon; 136 char *favicon;
135 char *footer; 137 char *footer;
136 char *index_header; 138 char *index_header;
137 char *index_info; 139 char *index_info;
138 char *logo; 140 char *logo;
139 char *logo_link; 141 char *logo_link;
140 char *module_link; 142 char *module_link;
141 char *repo_group; 143 char *repo_group;
142 char *robots; 144 char *robots;
143 char *root_title; 145 char *root_title;
144 char *root_desc; 146 char *root_desc;
145 char *root_readme; 147 char *root_readme;
146 char *script_name; 148 char *script_name;
147 char *virtual_root; 149 char *virtual_root;
148 int cache_size; 150 int cache_size;
149 int cache_dynamic_ttl; 151 int cache_dynamic_ttl;
150 int cache_max_create_time; 152 int cache_max_create_time;
151 int cache_repo_ttl; 153 int cache_repo_ttl;
152 int cache_root_ttl; 154 int cache_root_ttl;
153 int cache_static_ttl; 155 int cache_static_ttl;
154 int enable_index_links; 156 int enable_index_links;
155 int enable_log_filecount; 157 int enable_log_filecount;
156 int enable_log_linecount; 158 int enable_log_linecount;
157 int local_time; 159 int local_time;
158 int max_repo_count; 160 int max_repo_count;
159 int max_commit_count; 161 int max_commit_count;
160 int max_lock_attempts; 162 int max_lock_attempts;
161 int max_msg_len; 163 int max_msg_len;
162 int max_repodesc_len; 164 int max_repodesc_len;
165 int max_stats;
163 int nocache; 166 int nocache;
164 int renamelimit; 167 int renamelimit;
165 int snapshots; 168 int snapshots;
166 int summary_branches; 169 int summary_branches;
167 int summary_log; 170 int summary_log;
168 int summary_tags; 171 int summary_tags;
169}; 172};
170 173
171struct cgit_page { 174struct cgit_page {
172 time_t modified; 175 time_t modified;
173 time_t expires; 176 time_t expires;
174 size_t size; 177 size_t size;
175 char *mimetype; 178 char *mimetype;
176 char *charset; 179 char *charset;
177 char *filename; 180 char *filename;
178 char *title; 181 char *title;
179}; 182};
180 183
181struct cgit_context { 184struct cgit_context {
182 struct cgit_query qry; 185 struct cgit_query qry;
183 struct cgit_config cfg; 186 struct cgit_config cfg;
184 struct cgit_repo *repo; 187 struct cgit_repo *repo;
185 struct cgit_page page; 188 struct cgit_page page;
186}; 189};
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 7887b02..09f56a6 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -108,48 +108,53 @@ logo
108 Url which specifies the source of an image which will be used as a logo 108 Url which specifies the source of an image which will be used as a logo
109 on all cgit pages. 109 on all cgit pages.
110 110
111logo-link 111logo-link
112 Url loaded when clicking on the cgit logo image. If unspecified the 112 Url loaded when clicking on the cgit logo image. If unspecified the
113 calculated url of the repository index page will be used. Default 113 calculated url of the repository index page will be used. Default
114 value: none. 114 value: none.
115 115
116max-commit-count 116max-commit-count
117 Specifies the number of entries to list per page in "log" view. Default 117 Specifies the number of entries to list per page in "log" view. Default
118 value: "50". 118 value: "50".
119 119
120max-message-length 120max-message-length
121 Specifies the maximum number of commit message characters to display in 121 Specifies the maximum number of commit message characters to display in
122 "log" view. Default value: "80". 122 "log" view. Default value: "80".
123 123
124max-repo-count 124max-repo-count
125 Specifies the number of entries to list per page on therepository 125 Specifies the number of entries to list per page on therepository
126 index page. Default value: "50". 126 index page. Default value: "50".
127 127
128max-repodesc-length 128max-repodesc-length
129 Specifies the maximum number of repo description characters to display 129 Specifies the maximum number of repo description characters to display
130 on the repository index page. Default value: "80". 130 on the repository index page. Default value: "80".
131 131
132max-stats
133 Set the default maximum statistics period. Valid values are "week",
134 "month", "quarter" and "year". If unspecified, statistics are
135 disabled. Default value: none. See also: "repo.max-stats".
136
132module-link 137module-link
133 Text which will be used as the formatstring for a hyperlink when a 138 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 139 submodule is printed in a directory listing. The arguments for the
135 formatstring are the path and SHA1 of the submodule commit. Default 140 formatstring are the path and SHA1 of the submodule commit. Default
136 value: "./?repo=%s&page=commit&id=%s" 141 value: "./?repo=%s&page=commit&id=%s"
137 142
138nocache 143nocache
139 If set to the value "1" caching will be disabled. This settings is 144 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 145 deprecated, and will not be honored starting with cgit-1.0. Default
141 value: "0". 146 value: "0".
142 147
143renamelimit 148renamelimit
144 Maximum number of files to consider when detecting renames. The value 149 Maximum number of files to consider when detecting renames. The value
145 "-1" uses the compiletime value in git (for further info, look at 150 "-1" uses the compiletime value in git (for further info, look at
146 `man git-diff`). Default value: "-1". 151 `man git-diff`). Default value: "-1".
147 152
148repo.group 153repo.group
149 A value for the current repository group, which all repositories 154 A value for the current repository group, which all repositories
150 specified after this setting will inherit. Default value: none. 155 specified after this setting will inherit. Default value: none.
151 156
152robots 157robots
153 Text used as content for the "robots" meta-tag. Default value: 158 Text used as content for the "robots" meta-tag. Default value:
154 "index, nofollow". 159 "index, nofollow".
155 160
@@ -197,48 +202,53 @@ virtual-root
197 same kind of virtual urls, so this option will probably be deprecated. 202 same kind of virtual urls, so this option will probably be deprecated.
198 203
199REPOSITORY SETTINGS 204REPOSITORY SETTINGS
200------------------- 205-------------------
201repo.clone-url 206repo.clone-url
202 A list of space-separated urls which can be used to clone this repo. 207 A list of space-separated urls which can be used to clone this repo.
203 Default value: none. 208 Default value: none.
204 209
205repo.defbranch 210repo.defbranch
206 The name of the default branch for this repository. If no such branch 211 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 212 exists in the repository, the first branch name (when sorted) is used
208 as default instead. Default value: "master". 213 as default instead. Default value: "master".
209 214
210repo.desc 215repo.desc
211 The value to show as repository description. Default value: none. 216 The value to show as repository description. Default value: none.
212 217
213repo.enable-log-filecount 218repo.enable-log-filecount
214 A flag which can be used to disable the global setting 219 A flag which can be used to disable the global setting
215 `enable-log-filecount'. Default value: none. 220 `enable-log-filecount'. Default value: none.
216 221
217repo.enable-log-linecount 222repo.enable-log-linecount
218 A flag which can be used to disable the global setting 223 A flag which can be used to disable the global setting
219 `enable-log-linecount'. Default value: none. 224 `enable-log-linecount'. Default value: none.
220 225
226repo.max-stats
227 Override the default maximum statistics period. Valid values are equal
228 to the values specified for the global "max-stats" setting. Default
229 value: none.
230
221repo.name 231repo.name
222 The value to show as repository name. Default value: <repo.url>. 232 The value to show as repository name. Default value: <repo.url>.
223 233
224repo.owner 234repo.owner
225 A value used to identify the owner of the repository. Default value: 235 A value used to identify the owner of the repository. Default value:
226 none. 236 none.
227 237
228repo.path 238repo.path
229 An absolute path to the repository directory. For non-bare repositories 239 An absolute path to the repository directory. For non-bare repositories
230 this is the .git-directory. Default value: none. 240 this is the .git-directory. Default value: none.
231 241
232repo.readme 242repo.readme
233 A path (relative to <repo.path>) which specifies a file to include 243 A path (relative to <repo.path>) which specifies a file to include
234 verbatim as the "About" page for this repo. Default value: none. 244 verbatim as the "About" page for this repo. Default value: none.
235 245
236repo.snapshots 246repo.snapshots
237 A mask of allowed snapshot-formats for this repo, restricted by the 247 A mask of allowed snapshot-formats for this repo, restricted by the
238 "snapshots" global setting. Default value: <snapshots>. 248 "snapshots" global setting. Default value: <snapshots>.
239 249
240repo.url 250repo.url
241 The relative url used to access the repository. This must be the first 251 The relative url used to access the repository. This must be the first
242 setting specified for each repo. Default value: none. 252 setting specified for each repo. Default value: none.
243 253
244 254
@@ -255,62 +265,66 @@ clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git
255# Specify the css url 265# Specify the css url
256css=/css/cgit.css 266css=/css/cgit.css
257 267
258 268
259# Show extra links for each repository on the index page 269# Show extra links for each repository on the index page
260enable-index-links=1 270enable-index-links=1
261 271
262 272
263# Show number of affected files per commit on the log pages 273# Show number of affected files per commit on the log pages
264enable-log-filecount=1 274enable-log-filecount=1
265 275
266 276
267# Show number of added/removed lines per commit on the log pages 277# Show number of added/removed lines per commit on the log pages
268enable-log-linecount=1 278enable-log-linecount=1
269 279
270 280
271# Add a cgit favicon 281# Add a cgit favicon
272favicon=/favicon.ico 282favicon=/favicon.ico
273 283
274 284
275# Use a custom logo 285# Use a custom logo
276logo=/img/mylogo.png 286logo=/img/mylogo.png
277 287
278 288
289# Enable statistics per week, month and quarter
290max-stats=quarter
291
292
279# Set the title and heading of the repository index page 293# Set the title and heading of the repository index page
280root-title=foobar.com git repositories 294root-title=foobar.com git repositories
281 295
282 296
283# Set a subheading for the repository index page 297# Set a subheading for the repository index page
284root-desc=tracking the foobar development 298root-desc=tracking the foobar development
285 299
286 300
287# Include some more info about foobar.com on the index page 301# Include some more info about foobar.com on the index page
288root-readme=/var/www/htdocs/about.html 302root-readme=/var/www/htdocs/about.html
289 303
290 304
291# Allow download of tar.gz, tar.bz and zip-files 305# Allow download of tar.gz, tar.bz2 and zip-files
292snapshots=tar.gz tar.bz zip 306snapshots=tar.gz tar.bz2 zip
293 307
294 308
295## 309##
296## List of repositories. 310## List of repositories.
297## PS: Any repositories listed when repo.group is unset will not be 311## PS: Any repositories listed when repo.group is unset will not be
298## displayed under a group heading 312## displayed under a group heading
299## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos') 313## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos')
300## and included like this: 314## and included like this:
301## include=/etc/cgitrepos 315## include=/etc/cgitrepos
302## 316##
303 317
304 318
305repo.url=foo 319repo.url=foo
306repo.path=/pub/git/foo.git 320repo.path=/pub/git/foo.git
307repo.desc=the master foo repository 321repo.desc=the master foo repository
308repo.owner=fooman@foobar.com 322repo.owner=fooman@foobar.com
309repo.readme=info/web/about.html 323repo.readme=info/web/about.html
310 324
311 325
312repo.url=bar 326repo.url=bar
313repo.path=/pub/git/bar.git 327repo.path=/pub/git/bar.git
314repo.desc=the bars for your foo 328repo.desc=the bars for your foo
315repo.owner=barman@foobar.com 329repo.owner=barman@foobar.com
316repo.readme=info/web/about.html 330repo.readme=info/web/about.html
@@ -327,41 +341,44 @@ repo.desc=a set of extensions for bar users
327repo.url=wiz 341repo.url=wiz
328repo.path=/pub/git/wiz.git 342repo.path=/pub/git/wiz.git
329repo.desc=the wizard of foo 343repo.desc=the wizard of foo
330 344
331 345
332# Add some mirrored repositories 346# Add some mirrored repositories
333repo.group=mirrors 347repo.group=mirrors
334 348
335 349
336repo.url=git 350repo.url=git
337repo.path=/pub/git/git.git 351repo.path=/pub/git/git.git
338repo.desc=the dscm 352repo.desc=the dscm
339 353
340 354
341repo.url=linux 355repo.url=linux
342repo.path=/pub/git/linux.git 356repo.path=/pub/git/linux.git
343repo.desc=the kernel 357repo.desc=the kernel
344 358
345# Disable adhoc downloads of this repo 359# Disable adhoc downloads of this repo
346repo.snapshots=0 360repo.snapshots=0
347 361
348# Disable line-counts for this repo 362# Disable line-counts for this repo
349repo.enable-log-linecount=0 363repo.enable-log-linecount=0
350 364
365# Restrict the max statistics period for this repo
366repo.max-stats=month
367
351 368
352BUGS 369BUGS
353---- 370----
354Comments currently cannot appear on the same line as a setting; the comment 371Comments currently cannot appear on the same line as a setting; the comment
355will be included as part of the value. E.g. this line: 372will be included as part of the value. E.g. this line:
356 373
357 robots=index # allow indexing 374 robots=index # allow indexing
358 375
359will generate the following html element: 376will generate the following html element:
360 377
361 <meta name='robots' content='index # allow indexing'/> 378 <meta name='robots' content='index # allow indexing'/>
362 379
363 380
364 381
365AUTHOR 382AUTHOR
366------ 383------
367Lars Hjemli <hjemli@gmail.com> 384Lars Hjemli <hjemli@gmail.com>
diff --git a/cmd.c b/cmd.c
index 8914fa5..cf97da7 100644
--- a/cmd.c
+++ b/cmd.c
@@ -1,47 +1,48 @@
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{
@@ -87,78 +88,84 @@ static void repolist_fn(struct cgit_context *ctx)
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, ctx->qry.path, 108 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, ctx->qry.path,
108 ctx->repo->snapshots, ctx->qry.nohead); 109 ctx->repo->snapshots, ctx->qry.nohead);
109} 110}
110 111
112static void stats_fn(struct cgit_context *ctx)
113{
114 cgit_show_stats(ctx);
115}
116
111static void summary_fn(struct cgit_context *ctx) 117static void summary_fn(struct cgit_context *ctx)
112{ 118{
113 cgit_print_summary(); 119 cgit_print_summary();
114} 120}
115 121
116static void tag_fn(struct cgit_context *ctx) 122static void tag_fn(struct cgit_context *ctx)
117{ 123{
118 cgit_print_tag(ctx->qry.sha1); 124 cgit_print_tag(ctx->qry.sha1);
119} 125}
120 126
121static void tree_fn(struct cgit_context *ctx) 127static void tree_fn(struct cgit_context *ctx)
122{ 128{
123 cgit_print_tree(ctx->qry.sha1, ctx->qry.path); 129 cgit_print_tree(ctx->qry.sha1, ctx->qry.path);
124} 130}
125 131
126#define def_cmd(name, want_repo, want_layout) \ 132#define def_cmd(name, want_repo, want_layout) \
127 {#name, name##_fn, want_repo, want_layout} 133 {#name, name##_fn, want_repo, want_layout}
128 134
129struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx) 135struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx)
130{ 136{
131 static struct cgit_cmd cmds[] = { 137 static struct cgit_cmd cmds[] = {
132 def_cmd(HEAD, 1, 0), 138 def_cmd(HEAD, 1, 0),
133 def_cmd(atom, 1, 0), 139 def_cmd(atom, 1, 0),
134 def_cmd(about, 0, 1), 140 def_cmd(about, 0, 1),
135 def_cmd(blob, 1, 0), 141 def_cmd(blob, 1, 0),
136 def_cmd(commit, 1, 1), 142 def_cmd(commit, 1, 1),
137 def_cmd(diff, 1, 1), 143 def_cmd(diff, 1, 1),
138 def_cmd(info, 1, 0), 144 def_cmd(info, 1, 0),
139 def_cmd(log, 1, 1), 145 def_cmd(log, 1, 1),
140 def_cmd(ls_cache, 0, 0), 146 def_cmd(ls_cache, 0, 0),
141 def_cmd(objects, 1, 0), 147 def_cmd(objects, 1, 0),
142 def_cmd(patch, 1, 0), 148 def_cmd(patch, 1, 0),
143 def_cmd(plain, 1, 0), 149 def_cmd(plain, 1, 0),
144 def_cmd(refs, 1, 1), 150 def_cmd(refs, 1, 1),
145 def_cmd(repolist, 0, 0), 151 def_cmd(repolist, 0, 0),
146 def_cmd(snapshot, 1, 0), 152 def_cmd(snapshot, 1, 0),
153 def_cmd(stats, 1, 1),
147 def_cmd(summary, 1, 1), 154 def_cmd(summary, 1, 1),
148 def_cmd(tag, 1, 1), 155 def_cmd(tag, 1, 1),
149 def_cmd(tree, 1, 1), 156 def_cmd(tree, 1, 1),
150 }; 157 };
151 int i; 158 int i;
152 159
153 if (ctx->qry.page == NULL) { 160 if (ctx->qry.page == NULL) {
154 if (ctx->repo) 161 if (ctx->repo)
155 ctx->qry.page = "summary"; 162 ctx->qry.page = "summary";
156 else 163 else
157 ctx->qry.page = "repolist"; 164 ctx->qry.page = "repolist";
158 } 165 }
159 166
160 for(i = 0; i < sizeof(cmds)/sizeof(*cmds); i++) 167 for(i = 0; i < sizeof(cmds)/sizeof(*cmds); i++)
161 if (!strcmp(ctx->qry.page, cmds[i].name)) 168 if (!strcmp(ctx->qry.page, cmds[i].name))
162 return &cmds[i]; 169 return &cmds[i];
163 return NULL; 170 return NULL;
164} 171}
diff --git a/shared.c b/shared.c
index a764c4d..578a544 100644
--- a/shared.c
+++ b/shared.c
@@ -37,48 +37,49 @@ struct 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->max_stats = ctx.cfg.max_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 ret->mtime = -1; 64 ret->mtime = -1;
64 return ret; 65 return ret;
65} 66}
66 67
67struct cgit_repo *cgit_get_repoinfo(const char *url) 68struct cgit_repo *cgit_get_repoinfo(const char *url)
68{ 69{
69 int i; 70 int i;
70 struct cgit_repo *repo; 71 struct cgit_repo *repo;
71 72
72 for (i=0; i<cgit_repolist.count; i++) { 73 for (i=0; i<cgit_repolist.count; i++) {
73 repo = &cgit_repolist.repos[i]; 74 repo = &cgit_repolist.repos[i];
74 if (!strcmp(repo->url, url)) 75 if (!strcmp(repo->url, url))
75 return repo; 76 return repo;
76 } 77 }
77 return NULL; 78 return NULL;
78} 79}
79 80
80void *cgit_free_commitinfo(struct commitinfo *info) 81void *cgit_free_commitinfo(struct commitinfo *info)
81{ 82{
82 free(info->author); 83 free(info->author);
83 free(info->author_email); 84 free(info->author_email);
84 free(info->committer); 85 free(info->committer);
diff --git a/tests/t0107-snapshot.sh b/tests/t0107-snapshot.sh
index d97c465..8ab4912 100755
--- a/tests/t0107-snapshot.sh
+++ b/tests/t0107-snapshot.sh
@@ -1,39 +1,39 @@
1#!/bin/sh 1#!/bin/sh
2 2
3. ./setup.sh 3. ./setup.sh
4 4
5prepare_tests "Verify snapshot" 5prepare_tests "Verify snapshot"
6 6
7run_test 'get foo/snapshot/test.tar.gz' ' 7run_test 'get foo/snapshot/master.tar.gz' '
8 cgit_url "foo/snapshot/test.tar.gz" >trash/tmp 8 cgit_url "foo/snapshot/master.tar.gz" >trash/tmp
9' 9'
10 10
11run_test 'check html headers' ' 11run_test 'check html headers' '
12 head -n 1 trash/tmp | 12 head -n 1 trash/tmp |
13 grep -e "Content-Type: application/x-tar" && 13 grep -e "Content-Type: application/x-gzip" &&
14 14
15 head -n 2 trash/tmp | 15 head -n 2 trash/tmp |
16 grep -e "Content-Disposition: inline; filename=.test.tar.gz." 16 grep -e "Content-Disposition: inline; filename=.master.tar.gz."
17' 17'
18 18
19run_test 'strip off the header lines' ' 19run_test 'strip off the header lines' '
20 tail -n +6 trash/tmp > trash/test.tar.gz 20 tail -n +6 trash/tmp > trash/master.tar.gz
21' 21'
22 22
23run_test 'verify gzip format' 'gunzip --test trash/test.tar.gz' 23run_test 'verify gzip format' 'gunzip --test trash/master.tar.gz'
24run_test 'untar' ' 24run_test 'untar' '
25 rm -rf trash/foo && 25 rm -rf trash/master &&
26 tar -xf trash/test.tar.gz -C trash 26 tar -xf trash/master.tar.gz -C trash
27' 27'
28 28
29run_test 'count files' ' 29run_test 'count files' '
30 c=$(ls -1 trash/foo/ | wc -l) && 30 c=$(ls -1 trash/master/ | wc -l) &&
31 test $c = 5 31 test $c = 5
32' 32'
33 33
34run_test 'verify untarred file-5' ' 34run_test 'verify untarred file-5' '
35 grep -e "^5$" trash/foo/file-5 && 35 grep -e "^5$" trash/master/file-5 &&
36 test $(cat trash/foo/file-5 | wc -l) = 1 36 test $(cat trash/master/file-5 | wc -l) = 1
37' 37'
38 38
39tests_done 39tests_done
diff --git a/ui-log.c b/ui-log.c
index 2f90778..3202848 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -10,66 +10,99 @@
10#include "html.h" 10#include "html.h"
11#include "ui-shared.h" 11#include "ui-shared.h"
12 12
13int files, add_lines, rem_lines; 13int files, add_lines, rem_lines;
14 14
15void count_lines(char *line, int size) 15void count_lines(char *line, int size)
16{ 16{
17 if (size <= 0) 17 if (size <= 0)
18 return; 18 return;
19 19
20 if (line[0] == '+') 20 if (line[0] == '+')
21 add_lines++; 21 add_lines++;
22 22
23 else if (line[0] == '-') 23 else if (line[0] == '-')
24 rem_lines++; 24 rem_lines++;
25} 25}
26 26
27void inspect_files(struct diff_filepair *pair) 27void inspect_files(struct diff_filepair *pair)
28{ 28{
29 files++; 29 files++;
30 if (ctx.repo->enable_log_linecount) 30 if (ctx.repo->enable_log_linecount)
31 cgit_diff_files(pair->one->sha1, pair->two->sha1, count_lines); 31 cgit_diff_files(pair->one->sha1, pair->two->sha1, count_lines);
32} 32}
33 33
34void show_commit_decorations(struct commit *commit)
35{
36 struct name_decoration *deco;
37 static char buf[1024];
38
39 buf[sizeof(buf) - 1] = 0;
40 deco = lookup_decoration(&name_decoration, &commit->object);
41 while (deco) {
42 if (!prefixcmp(deco->name, "refs/heads/")) {
43 strncpy(buf, deco->name + 11, sizeof(buf) - 1);
44 cgit_log_link(buf, NULL, "branch-deco", buf, NULL, NULL,
45 0, NULL, NULL, ctx.qry.showmsg);
46 }
47 else if (!prefixcmp(deco->name, "tag: refs/tags/")) {
48 strncpy(buf, deco->name + 15, sizeof(buf) - 1);
49 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf);
50 }
51 else if (!prefixcmp(deco->name, "refs/remotes/")) {
52 strncpy(buf, deco->name + 13, sizeof(buf) - 1);
53 cgit_log_link(buf, NULL, "remote-deco", NULL,
54 sha1_to_hex(commit->object.sha1), NULL,
55 0, NULL, NULL, ctx.qry.showmsg);
56 }
57 else {
58 strncpy(buf, deco->name, sizeof(buf) - 1);
59 cgit_commit_link(buf, NULL, "deco", ctx.qry.head,
60 sha1_to_hex(commit->object.sha1));
61 }
62 deco = deco->next;
63 }
64}
65
34void print_commit(struct commit *commit) 66void print_commit(struct commit *commit)
35{ 67{
36 struct commitinfo *info; 68 struct commitinfo *info;
37 char *tmp; 69 char *tmp;
38 int cols = 2; 70 int cols = 2;
39 71
40 info = cgit_parse_commit(commit); 72 info = cgit_parse_commit(commit);
41 htmlf("<tr%s><td>", 73 htmlf("<tr%s><td>",
42 ctx.qry.showmsg ? " class='logheader'" : ""); 74 ctx.qry.showmsg ? " class='logheader'" : "");
43 tmp = fmt("id=%s", sha1_to_hex(commit->object.sha1)); 75 tmp = fmt("id=%s", sha1_to_hex(commit->object.sha1));
44 tmp = cgit_pageurl(ctx.repo->url, "commit", tmp); 76 tmp = cgit_pageurl(ctx.repo->url, "commit", tmp);
45 html_link_open(tmp, NULL, NULL); 77 html_link_open(tmp, NULL, NULL);
46 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE); 78 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE);
47 html_link_close(); 79 html_link_close();
48 htmlf("</td><td%s>", 80 htmlf("</td><td%s>",
49 ctx.qry.showmsg ? " class='logsubject'" : ""); 81 ctx.qry.showmsg ? " class='logsubject'" : "");
50 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head, 82 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head,
51 sha1_to_hex(commit->object.sha1)); 83 sha1_to_hex(commit->object.sha1));
84 show_commit_decorations(commit);
52 html("</td><td>"); 85 html("</td><td>");
53 html_txt(info->author); 86 html_txt(info->author);
54 if (ctx.repo->enable_log_filecount) { 87 if (ctx.repo->enable_log_filecount) {
55 files = 0; 88 files = 0;
56 add_lines = 0; 89 add_lines = 0;
57 rem_lines = 0; 90 rem_lines = 0;
58 cgit_diff_commit(commit, inspect_files); 91 cgit_diff_commit(commit, inspect_files);
59 html("</td><td>"); 92 html("</td><td>");
60 htmlf("%d", files); 93 htmlf("%d", files);
61 if (ctx.repo->enable_log_linecount) { 94 if (ctx.repo->enable_log_linecount) {
62 html("</td><td>"); 95 html("</td><td>");
63 htmlf("-%d/+%d", rem_lines, add_lines); 96 htmlf("-%d/+%d", rem_lines, add_lines);
64 } 97 }
65 } 98 }
66 html("</td></tr>\n"); 99 html("</td></tr>\n");
67 if (ctx.qry.showmsg) { 100 if (ctx.qry.showmsg) {
68 if (ctx.repo->enable_log_filecount) { 101 if (ctx.repo->enable_log_filecount) {
69 cols++; 102 cols++;
70 if (ctx.repo->enable_log_linecount) 103 if (ctx.repo->enable_log_linecount)
71 cols++; 104 cols++;
72 } 105 }
73 htmlf("<tr class='nohover'><td/><td colspan='%d' class='logmsg'>", 106 htmlf("<tr class='nohover'><td/><td colspan='%d' class='logmsg'>",
74 cols); 107 cols);
75 html_txt(info->msg); 108 html_txt(info->msg);
@@ -98,60 +131,63 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
98 const char *argv[] = {NULL, NULL, NULL, NULL, NULL}; 131 const char *argv[] = {NULL, NULL, NULL, NULL, NULL};
99 int argc = 2; 132 int argc = 2;
100 int i, columns = 3; 133 int i, columns = 3;
101 134
102 if (!tip) 135 if (!tip)
103 tip = ctx.qry.head; 136 tip = ctx.qry.head;
104 137
105 argv[1] = disambiguate_ref(tip); 138 argv[1] = disambiguate_ref(tip);
106 139
107 if (grep && pattern && (!strcmp(grep, "grep") || 140 if (grep && pattern && (!strcmp(grep, "grep") ||
108 !strcmp(grep, "author") || 141 !strcmp(grep, "author") ||
109 !strcmp(grep, "committer"))) 142 !strcmp(grep, "committer")))
110 argv[argc++] = fmt("--%s=%s", grep, pattern); 143 argv[argc++] = fmt("--%s=%s", grep, pattern);
111 144
112 if (path) { 145 if (path) {
113 argv[argc++] = "--"; 146 argv[argc++] = "--";
114 argv[argc++] = path; 147 argv[argc++] = path;
115 } 148 }
116 init_revisions(&rev, NULL); 149 init_revisions(&rev, NULL);
117 rev.abbrev = DEFAULT_ABBREV; 150 rev.abbrev = DEFAULT_ABBREV;
118 rev.commit_format = CMIT_FMT_DEFAULT; 151 rev.commit_format = CMIT_FMT_DEFAULT;
119 rev.verbose_header = 1; 152 rev.verbose_header = 1;
120 rev.show_root_diff = 0; 153 rev.show_root_diff = 0;
121 setup_revisions(argc, argv, &rev, NULL); 154 setup_revisions(argc, argv, &rev, NULL);
155 load_ref_decorations();
156 rev.show_decorations = 1;
122 rev.grep_filter.regflags |= REG_ICASE; 157 rev.grep_filter.regflags |= REG_ICASE;
123 compile_grep_patterns(&rev.grep_filter); 158 compile_grep_patterns(&rev.grep_filter);
124 prepare_revision_walk(&rev); 159 prepare_revision_walk(&rev);
125 160
126 if (pager) 161 if (pager)
127 html("<table class='list nowrap'>"); 162 html("<table class='list nowrap'>");
128 163
129 html("<tr class='nohover'><th class='left'>Age</th>" 164 html("<tr class='nohover'><th class='left'>Age</th>"
130 "<th class='left'>Commit message"); 165 "<th class='left'>Commit message");
131 if (pager) { 166 if (pager) {
132 html(" ("); 167 html(" (");
133 cgit_log_link("toggle", NULL, NULL, ctx.qry.head, ctx.qry.sha1, 168 cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL,
169 NULL, ctx.qry.head, ctx.qry.sha1,
134 ctx.qry.path, ctx.qry.ofs, ctx.qry.grep, 170 ctx.qry.path, ctx.qry.ofs, ctx.qry.grep,
135 ctx.qry.search, ctx.qry.showmsg ? 0 : 1); 171 ctx.qry.search, ctx.qry.showmsg ? 0 : 1);
136 html(")"); 172 html(")");
137 } 173 }
138 html("</th><th class='left'>Author</th>"); 174 html("</th><th class='left'>Author</th>");
139 if (ctx.repo->enable_log_filecount) { 175 if (ctx.repo->enable_log_filecount) {
140 html("<th class='left'>Files</th>"); 176 html("<th class='left'>Files</th>");
141 columns++; 177 columns++;
142 if (ctx.repo->enable_log_linecount) { 178 if (ctx.repo->enable_log_linecount) {
143 html("<th class='left'>Lines</th>"); 179 html("<th class='left'>Lines</th>");
144 columns++; 180 columns++;
145 } 181 }
146 } 182 }
147 html("</tr>\n"); 183 html("</tr>\n");
148 184
149 if (ofs<0) 185 if (ofs<0)
150 ofs = 0; 186 ofs = 0;
151 187
152 for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; i++) { 188 for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; i++) {
153 free(commit->buffer); 189 free(commit->buffer);
154 commit->buffer = NULL; 190 commit->buffer = NULL;
155 free_commit_list(commit->parents); 191 free_commit_list(commit->parents);
156 commit->parents = NULL; 192 commit->parents = NULL;
157 } 193 }
diff --git a/ui-refs.c b/ui-refs.c
index d61ee7c..25da00a 100644
--- a/ui-refs.c
+++ b/ui-refs.c
@@ -118,51 +118,51 @@ static int print_tag(struct refinfo *ref)
118 118
119 if (ref->object->type == OBJ_TAG) { 119 if (ref->object->type == OBJ_TAG) {
120 tag = (struct tag *)ref->object; 120 tag = (struct tag *)ref->object;
121 info = ref->tag; 121 info = ref->tag;
122 if (!tag || !info) 122 if (!tag || !info)
123 return 1; 123 return 1;
124 html("<tr><td>"); 124 html("<tr><td>");
125 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name); 125 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name);
126 html("</td><td>"); 126 html("</td><td>");
127 if (ctx.repo->snapshots && (tag->tagged->type == OBJ_COMMIT)) 127 if (ctx.repo->snapshots && (tag->tagged->type == OBJ_COMMIT))
128 print_tag_downloads(ctx.repo, name); 128 print_tag_downloads(ctx.repo, name);
129 else 129 else
130 cgit_object_link(tag->tagged); 130 cgit_object_link(tag->tagged);
131 html("</td><td>"); 131 html("</td><td>");
132 if (info->tagger) 132 if (info->tagger)
133 html(info->tagger); 133 html(info->tagger);
134 html("</td><td colspan='2'>"); 134 html("</td><td colspan='2'>");
135 if (info->tagger_date > 0) 135 if (info->tagger_date > 0)
136 cgit_print_age(info->tagger_date, -1, NULL); 136 cgit_print_age(info->tagger_date, -1, NULL);
137 html("</td></tr>\n"); 137 html("</td></tr>\n");
138 } else { 138 } else {
139 if (!header) 139 if (!header)
140 print_tag_header(); 140 print_tag_header();
141 html("<tr><td>"); 141 html("<tr><td>");
142 html_txt(name); 142 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name);
143 html("</td><td>"); 143 html("</td><td>");
144 if (ctx.repo->snapshots && (tag->tagged->type == OBJ_COMMIT)) 144 if (ctx.repo->snapshots && (ref->object->type == OBJ_COMMIT))
145 print_tag_downloads(ctx.repo, name); 145 print_tag_downloads(ctx.repo, name);
146 else 146 else
147 cgit_object_link(ref->object); 147 cgit_object_link(ref->object);
148 html("</td></tr>\n"); 148 html("</td></tr>\n");
149 } 149 }
150 return 0; 150 return 0;
151} 151}
152 152
153static void print_refs_link(char *path) 153static void print_refs_link(char *path)
154{ 154{
155 html("<tr class='nohover'><td colspan='4'>"); 155 html("<tr class='nohover'><td colspan='4'>");
156 cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path); 156 cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path);
157 html("</td></tr>"); 157 html("</td></tr>");
158} 158}
159 159
160void cgit_print_branches(int maxcount) 160void cgit_print_branches(int maxcount)
161{ 161{
162 struct reflist list; 162 struct reflist list;
163 int i; 163 int i;
164 164
165 html("<tr class='nohover'><th class='left'>Branch</th>" 165 html("<tr class='nohover'><th class='left'>Branch</th>"
166 "<th class='left'>Commit message</th>" 166 "<th class='left'>Commit message</th>"
167 "<th class='left'>Author</th>" 167 "<th class='left'>Author</th>"
168 "<th class='left' colspan='2'>Age</th></tr>\n"); 168 "<th class='left' colspan='2'>Age</th></tr>\n");
diff --git a/ui-shared.c b/ui-shared.c
index 95dfeb4..4f28512 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -348,48 +348,54 @@ void cgit_diff_link(char *name, char *title, char *class, char *head,
348 348
349 delim = repolink(title, class, "diff", head, path); 349 delim = repolink(title, class, "diff", head, path);
350 if (new_rev && strcmp(new_rev, ctx.qry.head)) { 350 if (new_rev && strcmp(new_rev, ctx.qry.head)) {
351 html(delim); 351 html(delim);
352 html("id="); 352 html("id=");
353 html_url_arg(new_rev); 353 html_url_arg(new_rev);
354 delim = "&amp;"; 354 delim = "&amp;";
355 } 355 }
356 if (old_rev) { 356 if (old_rev) {
357 html(delim); 357 html(delim);
358 html("id2="); 358 html("id2=");
359 html_url_arg(old_rev); 359 html_url_arg(old_rev);
360 } 360 }
361 html("'>"); 361 html("'>");
362 html_txt(name); 362 html_txt(name);
363 html("</a>"); 363 html("</a>");
364} 364}
365 365
366void cgit_patch_link(char *name, char *title, char *class, char *head, 366void cgit_patch_link(char *name, char *title, char *class, char *head,
367 char *rev) 367 char *rev)
368{ 368{
369 reporevlink("patch", name, title, class, head, rev, NULL); 369 reporevlink("patch", name, title, class, head, rev, NULL);
370} 370}
371 371
372void cgit_stats_link(char *name, char *title, char *class, char *head,
373 char *path)
374{
375 reporevlink("stats", name, title, class, head, NULL, path);
376}
377
372void cgit_object_link(struct object *obj) 378void cgit_object_link(struct object *obj)
373{ 379{
374 char *page, *shortrev, *fullrev, *name; 380 char *page, *shortrev, *fullrev, *name;
375 381
376 fullrev = sha1_to_hex(obj->sha1); 382 fullrev = sha1_to_hex(obj->sha1);
377 shortrev = xstrdup(fullrev); 383 shortrev = xstrdup(fullrev);
378 shortrev[10] = '\0'; 384 shortrev[10] = '\0';
379 if (obj->type == OBJ_COMMIT) { 385 if (obj->type == OBJ_COMMIT) {
380 cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL, 386 cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
381 ctx.qry.head, fullrev); 387 ctx.qry.head, fullrev);
382 return; 388 return;
383 } else if (obj->type == OBJ_TREE) 389 } else if (obj->type == OBJ_TREE)
384 page = "tree"; 390 page = "tree";
385 else if (obj->type == OBJ_TAG) 391 else if (obj->type == OBJ_TAG)
386 page = "tag"; 392 page = "tag";
387 else 393 else
388 page = "blob"; 394 page = "blob";
389 name = fmt("%s %s...", typename(obj->type), shortrev); 395 name = fmt("%s %s...", typename(obj->type), shortrev);
390 reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL); 396 reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
391} 397}
392 398
393void cgit_print_date(time_t secs, char *format, int local_time) 399void cgit_print_date(time_t secs, char *format, int local_time)
394{ 400{
395 char buf[64]; 401 char buf[64];
@@ -536,153 +542,161 @@ int print_archive_ref(const char *refname, const unsigned char *sha1,
536 return 1; 542 return 1;
537 if (obj->type == OBJ_TAG) { 543 if (obj->type == OBJ_TAG) {
538 tag = lookup_tag(sha1); 544 tag = lookup_tag(sha1);
539 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) 545 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag)))
540 return 0; 546 return 0;
541 hashcpy(fileid, tag->tagged->sha1); 547 hashcpy(fileid, tag->tagged->sha1);
542 } else if (obj->type != OBJ_BLOB) { 548 } else if (obj->type != OBJ_BLOB) {
543 return 0; 549 return 0;
544 } else { 550 } else {
545 hashcpy(fileid, sha1); 551 hashcpy(fileid, sha1);
546 } 552 }
547 if (!*header) { 553 if (!*header) {
548 html("<h1>download</h1>\n"); 554 html("<h1>download</h1>\n");
549 *header = 1; 555 *header = 1;
550 } 556 }
551 url = cgit_pageurl(ctx.qry.repo, "blob", 557 url = cgit_pageurl(ctx.qry.repo, "blob",
552 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid), 558 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid),
553 buf)); 559 buf));
554 html_link_open(url, NULL, "menu"); 560 html_link_open(url, NULL, "menu");
555 html_txt(strlpart(buf, 20)); 561 html_txt(strlpart(buf, 20));
556 html_link_close(); 562 html_link_close();
557 return 0; 563 return 0;
558} 564}
559 565
560void add_hidden_formfields(int incl_head, int incl_search, char *page) 566void cgit_add_hidden_formfields(int incl_head, int incl_search, char *page)
561{ 567{
562 char *url; 568 char *url;
563 569
564 if (!ctx.cfg.virtual_root) { 570 if (!ctx.cfg.virtual_root) {
565 url = fmt("%s/%s", ctx.qry.repo, page); 571 url = fmt("%s/%s", ctx.qry.repo, page);
566 if (ctx.qry.path) 572 if (ctx.qry.path)
567 url = fmt("%s/%s", url, ctx.qry.path); 573 url = fmt("%s/%s", url, ctx.qry.path);
568 html_hidden("url", url); 574 html_hidden("url", url);
569 } 575 }
570 576
571 if (incl_head && ctx.qry.head && ctx.repo->defbranch && 577 if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
572 strcmp(ctx.qry.head, ctx.repo->defbranch)) 578 strcmp(ctx.qry.head, ctx.repo->defbranch))
573 html_hidden("h", ctx.qry.head); 579 html_hidden("h", ctx.qry.head);
574 580
575 if (ctx.qry.sha1) 581 if (ctx.qry.sha1)
576 html_hidden("id", ctx.qry.sha1); 582 html_hidden("id", ctx.qry.sha1);
577 if (ctx.qry.sha2) 583 if (ctx.qry.sha2)
578 html_hidden("id2", ctx.qry.sha2); 584 html_hidden("id2", ctx.qry.sha2);
579 if (ctx.qry.showmsg) 585 if (ctx.qry.showmsg)
580 html_hidden("showmsg", "1"); 586 html_hidden("showmsg", "1");
581 587
582 if (incl_search) { 588 if (incl_search) {
583 if (ctx.qry.grep) 589 if (ctx.qry.grep)
584 html_hidden("qt", ctx.qry.grep); 590 html_hidden("qt", ctx.qry.grep);
585 if (ctx.qry.search) 591 if (ctx.qry.search)
586 html_hidden("q", ctx.qry.search); 592 html_hidden("q", ctx.qry.search);
587 } 593 }
588} 594}
589 595
596const char *fallback_cmd = "repolist";
597
590char *hc(struct cgit_cmd *cmd, const char *page) 598char *hc(struct cgit_cmd *cmd, const char *page)
591{ 599{
592 return (strcmp(cmd->name, page) ? NULL : "active"); 600 return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active");
593} 601}
594 602
595void cgit_print_pageheader(struct cgit_context *ctx) 603void cgit_print_pageheader(struct cgit_context *ctx)
596{ 604{
597 struct cgit_cmd *cmd = cgit_get_cmd(ctx); 605 struct cgit_cmd *cmd = cgit_get_cmd(ctx);
598 606
607 if (!cmd && ctx->repo)
608 fallback_cmd = "summary";
609
599 html("<table id='header'>\n"); 610 html("<table id='header'>\n");
600 html("<tr>\n"); 611 html("<tr>\n");
601 html("<td class='logo' rowspan='2'><a href='"); 612 html("<td class='logo' rowspan='2'><a href='");
602 if (ctx->cfg.logo_link) 613 if (ctx->cfg.logo_link)
603 html_attr(ctx->cfg.logo_link); 614 html_attr(ctx->cfg.logo_link);
604 else 615 else
605 html_attr(cgit_rooturl()); 616 html_attr(cgit_rooturl());
606 html("'><img src='"); 617 html("'><img src='");
607 html_attr(ctx->cfg.logo); 618 html_attr(ctx->cfg.logo);
608 html("' alt='cgit logo'/></a></td>\n"); 619 html("' alt='cgit logo'/></a></td>\n");
609 620
610 html("<td class='main'>"); 621 html("<td class='main'>");
611 if (ctx->repo) { 622 if (ctx->repo) {
612 cgit_index_link("index", NULL, NULL, NULL, 0); 623 cgit_index_link("index", NULL, NULL, NULL, 0);
613 html(" : "); 624 html(" : ");
614 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); 625 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
615 html("</td><td class='form'>"); 626 html("</td><td class='form'>");
616 html("<form method='get' action=''>\n"); 627 html("<form method='get' action=''>\n");
617 add_hidden_formfields(0, 1, ctx->qry.page); 628 cgit_add_hidden_formfields(0, 1, ctx->qry.page);
618 html("<select name='h' onchange='this.form.submit();'>\n"); 629 html("<select name='h' onchange='this.form.submit();'>\n");
619 for_each_branch_ref(print_branch_option, ctx->qry.head); 630 for_each_branch_ref(print_branch_option, ctx->qry.head);
620 html("</select> "); 631 html("</select> ");
621 html("<input type='submit' name='' value='switch'/>"); 632 html("<input type='submit' name='' value='switch'/>");
622 html("</form>"); 633 html("</form>");
623 } else 634 } else
624 html_txt(ctx->cfg.root_title); 635 html_txt(ctx->cfg.root_title);
625 html("</td></tr>\n"); 636 html("</td></tr>\n");
626 637
627 html("<tr><td class='sub'>"); 638 html("<tr><td class='sub'>");
628 if (ctx->repo) { 639 if (ctx->repo) {
629 html_txt(ctx->repo->desc); 640 html_txt(ctx->repo->desc);
630 html("</td><td class='sub right'>"); 641 html("</td><td class='sub right'>");
631 html_txt(ctx->repo->owner); 642 html_txt(ctx->repo->owner);
632 } else { 643 } else {
633 if (ctx->cfg.root_desc) 644 if (ctx->cfg.root_desc)
634 html_txt(ctx->cfg.root_desc); 645 html_txt(ctx->cfg.root_desc);
635 else if (ctx->cfg.index_info) 646 else if (ctx->cfg.index_info)
636 html_include(ctx->cfg.index_info); 647 html_include(ctx->cfg.index_info);
637 } 648 }
638 html("</td></tr></table>\n"); 649 html("</td></tr></table>\n");
639 650
640 html("<table class='tabs'><tr><td>\n"); 651 html("<table class='tabs'><tr><td>\n");
641 if (ctx->repo) { 652 if (ctx->repo) {
642 cgit_summary_link("summary", NULL, hc(cmd, "summary"), 653 cgit_summary_link("summary", NULL, hc(cmd, "summary"),
643 ctx->qry.head); 654 ctx->qry.head);
644 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head, 655 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head,
645 ctx->qry.sha1, NULL); 656 ctx->qry.sha1, NULL);
646 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head, 657 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head,
647 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg); 658 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg);
648 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 659 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
649 ctx->qry.sha1, NULL); 660 ctx->qry.sha1, NULL);
650 cgit_commit_link("commit", NULL, hc(cmd, "commit"), 661 cgit_commit_link("commit", NULL, hc(cmd, "commit"),
651 ctx->qry.head, ctx->qry.sha1); 662 ctx->qry.head, ctx->qry.sha1);
652 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 663 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
653 ctx->qry.sha1, ctx->qry.sha2, NULL); 664 ctx->qry.sha1, ctx->qry.sha2, NULL);
665 if (ctx->repo->max_stats)
666 cgit_stats_link("stats", NULL, hc(cmd, "stats"),
667 ctx->qry.head, NULL);
654 if (ctx->repo->readme) 668 if (ctx->repo->readme)
655 reporevlink("about", "about", NULL, 669 reporevlink("about", "about", NULL,
656 hc(cmd, "about"), ctx->qry.head, NULL, 670 hc(cmd, "about"), ctx->qry.head, NULL,
657 NULL); 671 NULL);
658 html("</td><td class='form'>"); 672 html("</td><td class='form'>");
659 html("<form class='right' method='get' action='"); 673 html("<form class='right' method='get' action='");
660 if (ctx->cfg.virtual_root) 674 if (ctx->cfg.virtual_root)
661 html_url_path(cgit_fileurl(ctx->qry.repo, "log", 675 html_url_path(cgit_fileurl(ctx->qry.repo, "log",
662 ctx->qry.path, NULL)); 676 ctx->qry.path, NULL));
663 html("'>\n"); 677 html("'>\n");
664 add_hidden_formfields(1, 0, "log"); 678 cgit_add_hidden_formfields(1, 0, "log");
665 html("<select name='qt'>\n"); 679 html("<select name='qt'>\n");
666 html_option("grep", "log msg", ctx->qry.grep); 680 html_option("grep", "log msg", ctx->qry.grep);
667 html_option("author", "author", ctx->qry.grep); 681 html_option("author", "author", ctx->qry.grep);
668 html_option("committer", "committer", ctx->qry.grep); 682 html_option("committer", "committer", ctx->qry.grep);
669 html("</select>\n"); 683 html("</select>\n");
670 html("<input class='txt' type='text' size='10' name='q' value='"); 684 html("<input class='txt' type='text' size='10' name='q' value='");
671 html_attr(ctx->qry.search); 685 html_attr(ctx->qry.search);
672 html("'/>\n"); 686 html("'/>\n");
673 html("<input type='submit' value='search'/>\n"); 687 html("<input type='submit' value='search'/>\n");
674 html("</form>\n"); 688 html("</form>\n");
675 } else { 689 } else {
676 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0); 690 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0);
677 if (ctx->cfg.root_readme) 691 if (ctx->cfg.root_readme)
678 site_link("about", "about", NULL, hc(cmd, "about"), 692 site_link("about", "about", NULL, hc(cmd, "about"),
679 NULL, 0); 693 NULL, 0);
680 html("</td><td class='form'>"); 694 html("</td><td class='form'>");
681 html("<form method='get' action='"); 695 html("<form method='get' action='");
682 html_attr(cgit_rooturl()); 696 html_attr(cgit_rooturl());
683 html("'>\n"); 697 html("'>\n");
684 html("<input type='text' name='q' size='10' value='"); 698 html("<input type='text' name='q' size='10' value='");
685 html_attr(ctx->qry.search); 699 html_attr(ctx->qry.search);
686 html("'/>\n"); 700 html("'/>\n");
687 html("<input type='submit' value='search'/>\n"); 701 html("<input type='submit' value='search'/>\n");
688 html("</form>"); 702 html("</form>");
diff --git a/ui-shared.h b/ui-shared.h
index 2ab53ae..5a3821f 100644
--- a/ui-shared.h
+++ b/ui-shared.h
@@ -9,38 +9,41 @@ extern char *cgit_pageurl(const char *reponame, const char *pagename,
9 const char *query); 9 const char *query);
10 10
11extern void cgit_index_link(char *name, char *title, char *class, 11extern void cgit_index_link(char *name, char *title, char *class,
12 char *pattern, int ofs); 12 char *pattern, int ofs);
13extern void cgit_summary_link(char *name, char *title, char *class, char *head); 13extern void cgit_summary_link(char *name, char *title, char *class, char *head);
14extern void cgit_tag_link(char *name, char *title, char *class, char *head, 14extern void cgit_tag_link(char *name, char *title, char *class, char *head,
15 char *rev); 15 char *rev);
16extern void cgit_tree_link(char *name, char *title, char *class, char *head, 16extern void cgit_tree_link(char *name, char *title, char *class, char *head,
17 char *rev, char *path); 17 char *rev, char *path);
18extern void cgit_plain_link(char *name, char *title, char *class, char *head, 18extern void cgit_plain_link(char *name, char *title, char *class, char *head,
19 char *rev, char *path); 19 char *rev, char *path);
20extern void cgit_log_link(char *name, char *title, char *class, char *head, 20extern void cgit_log_link(char *name, char *title, char *class, char *head,
21 char *rev, char *path, int ofs, char *grep, 21 char *rev, char *path, int ofs, char *grep,
22 char *pattern, int showmsg); 22 char *pattern, int showmsg);
23extern void cgit_commit_link(char *name, char *title, char *class, char *head, 23extern void cgit_commit_link(char *name, char *title, char *class, char *head,
24 char *rev); 24 char *rev);
25extern void cgit_patch_link(char *name, char *title, char *class, char *head, 25extern void cgit_patch_link(char *name, char *title, char *class, char *head,
26 char *rev); 26 char *rev);
27extern void cgit_refs_link(char *name, char *title, char *class, char *head, 27extern void cgit_refs_link(char *name, char *title, char *class, char *head,
28 char *rev, char *path); 28 char *rev, char *path);
29extern void cgit_snapshot_link(char *name, char *title, char *class, 29extern void cgit_snapshot_link(char *name, char *title, char *class,
30 char *head, char *rev, char *archivename); 30 char *head, char *rev, char *archivename);
31extern void cgit_diff_link(char *name, char *title, char *class, char *head, 31extern void cgit_diff_link(char *name, char *title, char *class, char *head,
32 char *new_rev, char *old_rev, char *path); 32 char *new_rev, char *old_rev, char *path);
33extern void cgit_stats_link(char *name, char *title, char *class, char *head,
34 char *path);
33extern void cgit_object_link(struct object *obj); 35extern void cgit_object_link(struct object *obj);
34 36
35extern void cgit_print_error(char *msg); 37extern void cgit_print_error(char *msg);
36extern void cgit_print_date(time_t secs, char *format, int local_time); 38extern void cgit_print_date(time_t secs, char *format, int local_time);
37extern void cgit_print_age(time_t t, time_t max_relative, char *format); 39extern void cgit_print_age(time_t t, time_t max_relative, char *format);
38extern void cgit_print_http_headers(struct cgit_context *ctx); 40extern void cgit_print_http_headers(struct cgit_context *ctx);
39extern void cgit_print_docstart(struct cgit_context *ctx); 41extern void cgit_print_docstart(struct cgit_context *ctx);
40extern void cgit_print_docend(); 42extern void cgit_print_docend();
41extern void cgit_print_pageheader(struct cgit_context *ctx); 43extern void cgit_print_pageheader(struct cgit_context *ctx);
42extern void cgit_print_filemode(unsigned short mode); 44extern void cgit_print_filemode(unsigned short mode);
43extern void cgit_print_snapshot_links(const char *repo, const char *head, 45extern void cgit_print_snapshot_links(const char *repo, const char *head,
44 const char *hex, int snapshots); 46 const char *hex, int snapshots);
45 47extern void cgit_add_hidden_formfields(int incl_head, int incl_search,
48 char *page);
46#endif /* UI_SHARED_H */ 49#endif /* UI_SHARED_H */
diff --git a/ui-snapshot.c b/ui-snapshot.c
index 6f09151..f25613e 100644
--- a/ui-snapshot.c
+++ b/ui-snapshot.c
@@ -37,50 +37,50 @@ static int write_compressed_tar_archive(struct archiver_args *args,const char *f
37 37
38 chk_zero(close(STDOUT_FILENO), "Closing STDOUT redirected to compressor"); 38 chk_zero(close(STDOUT_FILENO), "Closing STDOUT redirected to compressor");
39 chk_non_negative(dup2(stdout2,STDOUT_FILENO), "Restoring uncompressed STDOUT"); 39 chk_non_negative(dup2(stdout2,STDOUT_FILENO), "Restoring uncompressed STDOUT");
40 chk_zero(close(stdout2), "Closing uncompressed STDOUT"); 40 chk_zero(close(stdout2), "Closing uncompressed STDOUT");
41 chk_zero(close(rw[1]), "Closing write end of pipe in parent"); 41 chk_zero(close(rw[1]), "Closing write end of pipe in parent");
42 chk_positive(waitpid(gzpid,&status,0), "Waiting on compressor process"); 42 chk_positive(waitpid(gzpid,&status,0), "Waiting on compressor process");
43 if(! ( WIFEXITED(status) && WEXITSTATUS(status)==0 ) ) 43 if(! ( WIFEXITED(status) && WEXITSTATUS(status)==0 ) )
44 cgit_print_error("Failed to compress archive"); 44 cgit_print_error("Failed to compress archive");
45 45
46 return rv; 46 return rv;
47} 47}
48 48
49static int write_tar_gzip_archive(struct archiver_args *args) 49static int write_tar_gzip_archive(struct archiver_args *args)
50{ 50{
51 return write_compressed_tar_archive(args,"gzip"); 51 return write_compressed_tar_archive(args,"gzip");
52} 52}
53 53
54static int write_tar_bzip2_archive(struct archiver_args *args) 54static int write_tar_bzip2_archive(struct archiver_args *args)
55{ 55{
56 return write_compressed_tar_archive(args,"bzip2"); 56 return write_compressed_tar_archive(args,"bzip2");
57} 57}
58 58
59const struct cgit_snapshot_format cgit_snapshot_formats[] = { 59const struct cgit_snapshot_format cgit_snapshot_formats[] = {
60 { ".zip", "application/x-zip", write_zip_archive, 0x1 }, 60 { ".zip", "application/x-zip", write_zip_archive, 0x1 },
61 { ".tar.gz", "application/x-tar", write_tar_gzip_archive, 0x2 }, 61 { ".tar.gz", "application/x-gzip", write_tar_gzip_archive, 0x2 },
62 { ".tar.bz2", "application/x-tar", write_tar_bzip2_archive, 0x4 }, 62 { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive, 0x4 },
63 { ".tar", "application/x-tar", write_tar_archive, 0x8 }, 63 { ".tar", "application/x-tar", write_tar_archive, 0x8 },
64 {} 64 {}
65}; 65};
66 66
67static const struct cgit_snapshot_format *get_format(const char *filename) 67static const struct cgit_snapshot_format *get_format(const char *filename)
68{ 68{
69 const struct cgit_snapshot_format *fmt; 69 const struct cgit_snapshot_format *fmt;
70 int fl, sl; 70 int fl, sl;
71 71
72 fl = strlen(filename); 72 fl = strlen(filename);
73 for(fmt = cgit_snapshot_formats; fmt->suffix; fmt++) { 73 for(fmt = cgit_snapshot_formats; fmt->suffix; fmt++) {
74 sl = strlen(fmt->suffix); 74 sl = strlen(fmt->suffix);
75 if (sl >= fl) 75 if (sl >= fl)
76 continue; 76 continue;
77 if (!strcmp(fmt->suffix, filename + fl - sl)) 77 if (!strcmp(fmt->suffix, filename + fl - sl))
78 return fmt; 78 return fmt;
79 } 79 }
80 return NULL; 80 return NULL;
81} 81}
82 82
83static int make_snapshot(const struct cgit_snapshot_format *format, 83static int make_snapshot(const struct cgit_snapshot_format *format,
84 const char *hex, const char *prefix, 84 const char *hex, const char *prefix,
85 const char *filename) 85 const char *filename)
86{ 86{
@@ -154,39 +154,41 @@ static const char *get_ref_from_filename(const char *url, const char *filename,
154 return snapshot; 154 return snapshot;
155 155
156 return NULL; 156 return NULL;
157} 157}
158 158
159void cgit_print_snapshot(const char *head, const char *hex, 159void cgit_print_snapshot(const char *head, const char *hex,
160 const char *filename, int snapshots, int dwim) 160 const char *filename, int snapshots, int dwim)
161{ 161{
162 const struct cgit_snapshot_format* f; 162 const struct cgit_snapshot_format* f;
163 char *prefix = NULL; 163 char *prefix = NULL;
164 164
165 f = get_format(filename); 165 f = get_format(filename);
166 if (!f) { 166 if (!f) {
167 ctx.page.mimetype = "text/html"; 167 ctx.page.mimetype = "text/html";
168 cgit_print_http_headers(&ctx); 168 cgit_print_http_headers(&ctx);
169 cgit_print_docstart(&ctx); 169 cgit_print_docstart(&ctx);
170 cgit_print_pageheader(&ctx); 170 cgit_print_pageheader(&ctx);
171 cgit_print_error(fmt("Unsupported snapshot format: %s", filename)); 171 cgit_print_error(fmt("Unsupported snapshot format: %s", filename));
172 cgit_print_docend(); 172 cgit_print_docend();
173 return; 173 return;
174 } 174 }
175 175
176 if (!hex && dwim) { 176 if (!hex && dwim) {
177 hex = get_ref_from_filename(ctx.repo->url, filename, f); 177 hex = get_ref_from_filename(ctx.repo->url, filename, f);
178 if (hex != NULL) { 178 if (hex == NULL) {
179 html_status(404, "Not found", 0);
180 return;
181 }
179 prefix = xstrdup(filename); 182 prefix = xstrdup(filename);
180 prefix[strlen(filename) - strlen(f->suffix)] = '\0'; 183 prefix[strlen(filename) - strlen(f->suffix)] = '\0';
181 } 184 }
182 }
183 185
184 if (!hex) 186 if (!hex)
185 hex = head; 187 hex = head;
186 188
187 if (!prefix) 189 if (!prefix)
188 prefix = xstrdup(cgit_repobasename(ctx.repo->url)); 190 prefix = xstrdup(cgit_repobasename(ctx.repo->url));
189 191
190 make_snapshot(f, hex, prefix, filename); 192 make_snapshot(f, hex, prefix, filename);
191 free(prefix); 193 free(prefix);
192} 194}
diff --git a/ui-stats.c b/ui-stats.c
new file mode 100644
index 0000000..9fc06d3
--- a/dev/null
+++ b/ui-stats.c
@@ -0,0 +1,410 @@
1#include <string-list.h>
2
3#include "cgit.h"
4#include "html.h"
5#include "ui-shared.h"
6#include "ui-stats.h"
7
8#define MONTHS 6
9
10struct authorstat {
11 long total;
12 struct string_list list;
13};
14
15#define DAY_SECS (60 * 60 * 24)
16#define WEEK_SECS (DAY_SECS * 7)
17
18static void trunc_week(struct tm *tm)
19{
20 time_t t = timegm(tm);
21 t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
22 gmtime_r(&t, tm);
23}
24
25static void dec_week(struct tm *tm)
26{
27 time_t t = timegm(tm);
28 t -= WEEK_SECS;
29 gmtime_r(&t, tm);
30}
31
32static void inc_week(struct tm *tm)
33{
34 time_t t = timegm(tm);
35 t += WEEK_SECS;
36 gmtime_r(&t, tm);
37}
38
39static char *pretty_week(struct tm *tm)
40{
41 static char buf[10];
42
43 strftime(buf, sizeof(buf), "W%V %G", tm);
44 return buf;
45}
46
47static void trunc_month(struct tm *tm)
48{
49 tm->tm_mday = 1;
50}
51
52static void dec_month(struct tm *tm)
53{
54 tm->tm_mon--;
55 if (tm->tm_mon < 0) {
56 tm->tm_year--;
57 tm->tm_mon = 11;
58 }
59}
60
61static void inc_month(struct tm *tm)
62{
63 tm->tm_mon++;
64 if (tm->tm_mon > 11) {
65 tm->tm_year++;
66 tm->tm_mon = 0;
67 }
68}
69
70static char *pretty_month(struct tm *tm)
71{
72 static const char *months[] = {
73 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
74 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
75 };
76 return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
77}
78
79static void trunc_quarter(struct tm *tm)
80{
81 trunc_month(tm);
82 while(tm->tm_mon % 3 != 0)
83 dec_month(tm);
84}
85
86static void dec_quarter(struct tm *tm)
87{
88 dec_month(tm);
89 dec_month(tm);
90 dec_month(tm);
91}
92
93static void inc_quarter(struct tm *tm)
94{
95 inc_month(tm);
96 inc_month(tm);
97 inc_month(tm);
98}
99
100static char *pretty_quarter(struct tm *tm)
101{
102 return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
103}
104
105static void trunc_year(struct tm *tm)
106{
107 trunc_month(tm);
108 tm->tm_mon = 0;
109}
110
111static void dec_year(struct tm *tm)
112{
113 tm->tm_year--;
114}
115
116static void inc_year(struct tm *tm)
117{
118 tm->tm_year++;
119}
120
121static char *pretty_year(struct tm *tm)
122{
123 return fmt("%d", tm->tm_year + 1900);
124}
125
126struct cgit_period periods[] = {
127 {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
128 {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
129 {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
130 {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
131};
132
133/* Given a period code or name, return a period index (1, 2, 3 or 4)
134 * and update the period pointer to the correcsponding struct.
135 * If no matching code is found, return 0.
136 */
137int cgit_find_stats_period(const char *expr, struct cgit_period **period)
138{
139 int i;
140 char code = '\0';
141
142 if (!expr)
143 return 0;
144
145 if (strlen(expr) == 1)
146 code = expr[0];
147
148 for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
149 if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
150 if (period)
151 *period = &periods[i];
152 return i+1;
153 }
154 return 0;
155}
156
157static void add_commit(struct string_list *authors, struct commit *commit,
158 struct cgit_period *period)
159{
160 struct commitinfo *info;
161 struct string_list_item *author, *item;
162 struct authorstat *authorstat;
163 struct string_list *items;
164 char *tmp;
165 struct tm *date;
166 time_t t;
167
168 info = cgit_parse_commit(commit);
169 tmp = xstrdup(info->author);
170 author = string_list_insert(tmp, authors);
171 if (!author->util)
172 author->util = xcalloc(1, sizeof(struct authorstat));
173 else
174 free(tmp);
175 authorstat = author->util;
176 items = &authorstat->list;
177 t = info->committer_date;
178 date = gmtime(&t);
179 period->trunc(date);
180 tmp = xstrdup(period->pretty(date));
181 item = string_list_insert(tmp, items);
182 if (item->util)
183 free(tmp);
184 item->util++;
185 authorstat->total++;
186 cgit_free_commitinfo(info);
187}
188
189static int cmp_total_commits(const void *a1, const void *a2)
190{
191 const struct string_list_item *i1 = a1;
192 const struct string_list_item *i2 = a2;
193 const struct authorstat *auth1 = i1->util;
194 const struct authorstat *auth2 = i2->util;
195
196 return auth2->total - auth1->total;
197}
198
199/* Walk the commit DAG and collect number of commits per author per
200 * timeperiod into a nested string_list collection.
201 */
202struct string_list collect_stats(struct cgit_context *ctx,
203 struct cgit_period *period)
204{
205 struct string_list authors;
206 struct rev_info rev;
207 struct commit *commit;
208 const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL};
209 int argc = 3;
210 time_t now;
211 long i;
212 struct tm *tm;
213 char tmp[11];
214
215 time(&now);
216 tm = gmtime(&now);
217 period->trunc(tm);
218 for (i = 1; i < period->count; i++)
219 period->dec(tm);
220 strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
221 argv[2] = xstrdup(fmt("--since=%s", tmp));
222 if (ctx->qry.path) {
223 argv[3] = "--";
224 argv[4] = ctx->qry.path;
225 argc += 2;
226 }
227 init_revisions(&rev, NULL);
228 rev.abbrev = DEFAULT_ABBREV;
229 rev.commit_format = CMIT_FMT_DEFAULT;
230 rev.no_merges = 1;
231 rev.verbose_header = 1;
232 rev.show_root_diff = 0;
233 setup_revisions(argc, argv, &rev, NULL);
234 prepare_revision_walk(&rev);
235 memset(&authors, 0, sizeof(authors));
236 while ((commit = get_revision(&rev)) != NULL) {
237 add_commit(&authors, commit, period);
238 free(commit->buffer);
239 free_commit_list(commit->parents);
240 }
241 return authors;
242}
243
244void print_combined_authorrow(struct string_list *authors, int from, int to,
245 const char *name, const char *leftclass, const char *centerclass,
246 const char *rightclass, struct cgit_period *period)
247{
248 struct string_list_item *author;
249 struct authorstat *authorstat;
250 struct string_list *items;
251 struct string_list_item *date;
252 time_t now;
253 long i, j, total, subtotal;
254 struct tm *tm;
255 char *tmp;
256
257 time(&now);
258 tm = gmtime(&now);
259 period->trunc(tm);
260 for (i = 1; i < period->count; i++)
261 period->dec(tm);
262
263 total = 0;
264 htmlf("<tr><td class='%s'>%s</td>", leftclass,
265 fmt(name, to - from + 1));
266 for (j = 0; j < period->count; j++) {
267 tmp = period->pretty(tm);
268 period->inc(tm);
269 subtotal = 0;
270 for (i = from; i <= to; i++) {
271 author = &authors->items[i];
272 authorstat = author->util;
273 items = &authorstat->list;
274 date = string_list_lookup(tmp, items);
275 if (date)
276 subtotal += (size_t)date->util;
277 }
278 htmlf("<td class='%s'>%d</td>", centerclass, subtotal);
279 total += subtotal;
280 }
281 htmlf("<td class='%s'>%d</td></tr>", rightclass, total);
282}
283
284void print_authors(struct string_list *authors, int top,
285 struct cgit_period *period)
286{
287 struct string_list_item *author;
288 struct authorstat *authorstat;
289 struct string_list *items;
290 struct string_list_item *date;
291 time_t now;
292 long i, j, total;
293 struct tm *tm;
294 char *tmp;
295
296 time(&now);
297 tm = gmtime(&now);
298 period->trunc(tm);
299 for (i = 1; i < period->count; i++)
300 period->dec(tm);
301
302 html("<table class='stats'><tr><th>Author</th>");
303 for (j = 0; j < period->count; j++) {
304 tmp = period->pretty(tm);
305 htmlf("<th>%s</th>", tmp);
306 period->inc(tm);
307 }
308 html("<th>Total</th></tr>\n");
309
310 if (top <= 0 || top > authors->nr)
311 top = authors->nr;
312
313 for (i = 0; i < top; i++) {
314 author = &authors->items[i];
315 html("<tr><td class='left'>");
316 html_txt(author->string);
317 html("</td>");
318 authorstat = author->util;
319 items = &authorstat->list;
320 total = 0;
321 for (j = 0; j < period->count; j++)
322 period->dec(tm);
323 for (j = 0; j < period->count; j++) {
324 tmp = period->pretty(tm);
325 period->inc(tm);
326 date = string_list_lookup(tmp, items);
327 if (!date)
328 html("<td>0</td>");
329 else {
330 htmlf("<td>%d</td>", date->util);
331 total += (size_t)date->util;
332 }
333 }
334 htmlf("<td class='sum'>%d</td></tr>", total);
335 }
336
337 if (top < authors->nr)
338 print_combined_authorrow(authors, top, authors->nr - 1,
339 "Others (%d)", "left", "", "sum", period);
340
341 print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
342 "total", "sum", "sum", period);
343 html("</table>");
344}
345
346/* Create a sorted string_list with one entry per author. The util-field
347 * for each author is another string_list which is used to calculate the
348 * number of commits per time-interval.
349 */
350void cgit_show_stats(struct cgit_context *ctx)
351{
352 struct string_list authors;
353 struct cgit_period *period;
354 int top, i;
355 const char *code = "w";
356
357 if (ctx->qry.period)
358 code = ctx->qry.period;
359
360 i = cgit_find_stats_period(code, &period);
361 if (!i) {
362 cgit_print_error(fmt("Unknown statistics type: %c", code));
363 return;
364 }
365 if (i > ctx->repo->max_stats) {
366 cgit_print_error(fmt("Statistics type disabled: %s",
367 period->name));
368 return;
369 }
370 authors = collect_stats(ctx, period);
371 qsort(authors.items, authors.nr, sizeof(struct string_list_item),
372 cmp_total_commits);
373
374 top = ctx->qry.ofs;
375 if (!top)
376 top = 10;
377 htmlf("<h2>Commits per author per %s", period->name);
378 if (ctx->qry.path) {
379 html(" (path '");
380 html_txt(ctx->qry.path);
381 html("')");
382 }
383 html("</h2>");
384
385 html("<form method='get' action='' style='float: right; text-align: right;'>");
386 cgit_add_hidden_formfields(1, 0, "stats");
387 if (ctx->repo->max_stats > 1) {
388 html("Period: ");
389 html("<select name='period' onchange='this.form.submit();'>");
390 for (i = 0; i < ctx->repo->max_stats; i++)
391 htmlf("<option value='%c'%s>%s</option>",
392 periods[i].code,
393 period == &periods[i] ? " selected" : "",
394 periods[i].name);
395 html("</select><br/><br/>");
396 }
397 html("Authors: ");
398 html("");
399 html("<select name='ofs' onchange='this.form.submit();'>");
400 htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : "");
401 htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : "");
402 htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : "");
403 htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : "");
404 htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : "");
405 html("</select>");
406 html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>");
407 html("</form>");
408 print_authors(&authors, top, period);
409}
410
diff --git a/ui-stats.h b/ui-stats.h
new file mode 100644
index 0000000..4f13dba
--- a/dev/null
+++ b/ui-stats.h
@@ -0,0 +1,27 @@
1#ifndef UI_STATS_H
2#define UI_STATS_H
3
4#include "cgit.h"
5
6struct cgit_period {
7 const char code;
8 const char *name;
9 int max_periods;
10 int count;
11
12 /* Convert a tm value to the first day in the period */
13 void (*trunc)(struct tm *tm);
14
15 /* Update tm value to start of next/previous period */
16 void (*dec)(struct tm *tm);
17 void (*inc)(struct tm *tm);
18
19 /* Pretty-print a tm value */
20 char *(*pretty)(struct tm *tm);
21};
22
23extern int cgit_find_stats_period(const char *expr, struct cgit_period **period);
24
25extern void cgit_show_stats(struct cgit_context *ctx);
26
27#endif /* UI_STATS_H */
diff --git a/ui-tag.c b/ui-tag.c
index 3aea87d..0e056e0 100644
--- a/ui-tag.c
+++ b/ui-tag.c
@@ -32,48 +32,58 @@ static void print_tag_content(char *buf)
32 32
33void cgit_print_tag(char *revname) 33void cgit_print_tag(char *revname)
34{ 34{
35 unsigned char sha1[20]; 35 unsigned char sha1[20];
36 struct object *obj; 36 struct object *obj;
37 struct tag *tag; 37 struct tag *tag;
38 struct taginfo *info; 38 struct taginfo *info;
39 39
40 if (get_sha1(revname, sha1)) { 40 if (get_sha1(revname, sha1)) {
41 cgit_print_error(fmt("Bad tag reference: %s", revname)); 41 cgit_print_error(fmt("Bad tag reference: %s", revname));
42 return; 42 return;
43 } 43 }
44 obj = parse_object(sha1); 44 obj = parse_object(sha1);
45 if (!obj) { 45 if (!obj) {
46 cgit_print_error(fmt("Bad object id: %s", sha1_to_hex(sha1))); 46 cgit_print_error(fmt("Bad object id: %s", sha1_to_hex(sha1)));
47 return; 47 return;
48 } 48 }
49 if (obj->type == OBJ_TAG) { 49 if (obj->type == OBJ_TAG) {
50 tag = lookup_tag(sha1); 50 tag = lookup_tag(sha1);
51 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) { 51 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) {
52 cgit_print_error(fmt("Bad tag object: %s", revname)); 52 cgit_print_error(fmt("Bad tag object: %s", revname));
53 return; 53 return;
54 } 54 }
55 html("<table class='commit-info'>\n"); 55 html("<table class='commit-info'>\n");
56 htmlf("<tr><td>Tag name</td><td>%s (%s)</td></tr>\n", 56 htmlf("<tr><td>Tag name</td><td>");
57 revname, sha1_to_hex(sha1)); 57 html_txt(revname);
58 htmlf(" (%s)</td></tr>\n", sha1_to_hex(sha1));
58 if (info->tagger_date > 0) { 59 if (info->tagger_date > 0) {
59 html("<tr><td>Tag date</td><td>"); 60 html("<tr><td>Tag date</td><td>");
60 cgit_print_date(info->tagger_date, FMT_LONGDATE, ctx.cfg.local_time); 61 cgit_print_date(info->tagger_date, FMT_LONGDATE, ctx.cfg.local_time);
61 html("</td></tr>\n"); 62 html("</td></tr>\n");
62 } 63 }
63 if (info->tagger) { 64 if (info->tagger) {
64 html("<tr><td>Tagged by</td><td>"); 65 html("<tr><td>Tagged by</td><td>");
65 html_txt(info->tagger); 66 html_txt(info->tagger);
66 if (info->tagger_email) { 67 if (info->tagger_email) {
67 html(" "); 68 html(" ");
68 html_txt(info->tagger_email); 69 html_txt(info->tagger_email);
69 } 70 }
70 html("</td></tr>\n"); 71 html("</td></tr>\n");
71 } 72 }
72 html("<tr><td>Tagged object</td><td>"); 73 html("<tr><td>Tagged object</td><td>");
73 cgit_object_link(tag->tagged); 74 cgit_object_link(tag->tagged);
74 html("</td></tr>\n"); 75 html("</td></tr>\n");
75 html("</table>\n"); 76 html("</table>\n");
76 print_tag_content(info->msg); 77 print_tag_content(info->msg);
78 } else {
79 html("<table class='commit-info'>\n");
80 htmlf("<tr><td>Tag name</td><td>");
81 html_txt(revname);
82 html("</td></tr>\n");
83 html("<tr><td>Tagged object</td><td>");
84 cgit_object_link(obj);
85 html("</td></tr>\n");
86 html("</table>\n");
77 } 87 }
78 return; 88 return;
79} 89}
diff --git a/ui-tree.c b/ui-tree.c
index 2a8625c..c26ba4c 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -89,48 +89,51 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
89 89
90 html("<tr><td class='ls-mode'>"); 90 html("<tr><td class='ls-mode'>");
91 cgit_print_filemode(mode); 91 cgit_print_filemode(mode);
92 html("</td><td>"); 92 html("</td><td>");
93 if (S_ISGITLINK(mode)) { 93 if (S_ISGITLINK(mode)) {
94 htmlf("<a class='ls-mod' href='"); 94 htmlf("<a class='ls-mod' href='");
95 html_attr(fmt(ctx.repo->module_link, 95 html_attr(fmt(ctx.repo->module_link,
96 name, 96 name,
97 sha1_to_hex(sha1))); 97 sha1_to_hex(sha1)));
98 html("'>"); 98 html("'>");
99 html_txt(name); 99 html_txt(name);
100 html("</a>"); 100 html("</a>");
101 } else if (S_ISDIR(mode)) { 101 } else if (S_ISDIR(mode)) {
102 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, 102 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head,
103 curr_rev, fullpath); 103 curr_rev, fullpath);
104 } else { 104 } else {
105 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head, 105 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head,
106 curr_rev, fullpath); 106 curr_rev, fullpath);
107 } 107 }
108 htmlf("</td><td class='ls-size'>%li</td>", size); 108 htmlf("</td><td class='ls-size'>%li</td>", size);
109 109
110 html("<td>"); 110 html("<td>");
111 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev, 111 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev,
112 fullpath, 0, NULL, NULL, ctx.qry.showmsg); 112 fullpath, 0, NULL, NULL, ctx.qry.showmsg);
113 if (ctx.repo->max_stats)
114 cgit_stats_link("stats", NULL, "button", ctx.qry.head,
115 fullpath);
113 html("</td></tr>\n"); 116 html("</td></tr>\n");
114 free(name); 117 free(name);
115 return 0; 118 return 0;
116} 119}
117 120
118static void ls_head() 121static void ls_head()
119{ 122{
120 html("<table summary='tree listing' class='list'>\n"); 123 html("<table summary='tree listing' class='list'>\n");
121 html("<tr class='nohover'>"); 124 html("<tr class='nohover'>");
122 html("<th class='left'>Mode</th>"); 125 html("<th class='left'>Mode</th>");
123 html("<th class='left'>Name</th>"); 126 html("<th class='left'>Name</th>");
124 html("<th class='right'>Size</th>"); 127 html("<th class='right'>Size</th>");
125 html("<th/>"); 128 html("<th/>");
126 html("</tr>\n"); 129 html("</tr>\n");
127 header = 1; 130 header = 1;
128} 131}
129 132
130static void ls_tail() 133static void ls_tail()
131{ 134{
132 if (!header) 135 if (!header)
133 return; 136 return;
134 html("</table>\n"); 137 html("</table>\n");
135 header = 0; 138 header = 0;
136} 139}