summaryrefslogtreecommitdiffabout
authorLars Hjemli <hjemli@gmail.com>2009-01-27 19:16:37 (UTC)
committer Lars Hjemli <hjemli@gmail.com>2009-01-27 19:16:37 (UTC)
commit7710178e45dee61e85ea77c4221309ce8c086f95 (patch) (unidiff)
tree281c5251777308f18c05d323183b28470445f4bc
parente78186dcb63ec67a38dddfcd8f91d2108583320b (diff)
parentb54ef9749c083afd86573112fad3b3ed8ee2d0e4 (diff)
downloadcgit-7710178e45dee61e85ea77c4221309ce8c086f95.zip
cgit-7710178e45dee61e85ea77c4221309ce8c086f95.tar.gz
cgit-7710178e45dee61e85ea77c4221309ce8c086f95.tar.bz2
Merge branch 'lh/stats'
Conflicts: cgit.c cgit.css cgit.h ui-tree.c Signed-off-by: Lars Hjemli <hjemli@gmail.com>
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--Makefile1
-rw-r--r--cgit.c8
-rw-r--r--cgit.css76
-rw-r--r--cgit.h3
-rw-r--r--cgitrc.5.txt17
-rw-r--r--cmd.c7
-rw-r--r--shared.c1
-rw-r--r--ui-shared.c15
-rw-r--r--ui-shared.h5
-rw-r--r--ui-stats.c410
-rw-r--r--ui-stats.h27
-rw-r--r--ui-tree.c3
12 files changed, 569 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index 7793c0b..a52285e 100644
--- a/Makefile
+++ b/Makefile
@@ -29,127 +29,128 @@ ifeq ($(uname_O),Cygwin)
29 NEEDS_LIBICONV = YesPlease 29 NEEDS_LIBICONV = YesPlease
30endif 30endif
31 31
32# 32#
33# Let the user override the above settings. 33# Let the user override the above settings.
34# 34#
35-include cgit.conf 35-include cgit.conf
36 36
37# 37#
38# Define a way to invoke make in subdirs quietly, shamelessly ripped 38# Define a way to invoke make in subdirs quietly, shamelessly ripped
39# from git.git 39# from git.git
40# 40#
41QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir 41QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir
42QUIET_SUBDIR1 = 42QUIET_SUBDIR1 =
43 43
44ifneq ($(findstring $(MAKEFLAGS),w),w) 44ifneq ($(findstring $(MAKEFLAGS),w),w)
45PRINT_DIR = --no-print-directory 45PRINT_DIR = --no-print-directory
46else # "make -w" 46else # "make -w"
47NO_SUBDIR = : 47NO_SUBDIR = :
48endif 48endif
49 49
50ifndef V 50ifndef V
51 QUIET_CC = @echo ' ' CC $@; 51 QUIET_CC = @echo ' ' CC $@;
52 QUIET_MM = @echo ' ' MM $@; 52 QUIET_MM = @echo ' ' MM $@;
53 QUIET_SUBDIR0 = +@subdir= 53 QUIET_SUBDIR0 = +@subdir=
54 QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \ 54 QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \
55 $(MAKE) $(PRINT_DIR) -C $$subdir 55 $(MAKE) $(PRINT_DIR) -C $$subdir
56endif 56endif
57 57
58# 58#
59# Define a pattern rule for automatic dependency building 59# Define a pattern rule for automatic dependency building
60# 60#
61%.d: %.c 61%.d: %.c
62 $(QUIET_MM)$(CC) $(CFLAGS) -MM $< | sed -e 's/\($*\)\.o:/\1.o $@:/g' >$@ 62 $(QUIET_MM)$(CC) $(CFLAGS) -MM $< | sed -e 's/\($*\)\.o:/\1.o $@:/g' >$@
63 63
64# 64#
65# Define a pattern rule for silent object building 65# Define a pattern rule for silent object building
66# 66#
67%.o: %.c 67%.o: %.c
68 $(QUIET_CC)$(CC) -o $*.o -c $(CFLAGS) $< 68 $(QUIET_CC)$(CC) -o $*.o -c $(CFLAGS) $<
69 69
70 70
71EXTLIBS = git/libgit.a git/xdiff/lib.a -lz -lcrypto 71EXTLIBS = git/libgit.a git/xdiff/lib.a -lz -lcrypto
72OBJECTS = 72OBJECTS =
73OBJECTS += cache.o 73OBJECTS += cache.o
74OBJECTS += cgit.o 74OBJECTS += cgit.o
75OBJECTS += cmd.o 75OBJECTS += cmd.o
76OBJECTS += configfile.o 76OBJECTS += configfile.o
77OBJECTS += html.o 77OBJECTS += html.o
78OBJECTS += parsing.o 78OBJECTS += parsing.o
79OBJECTS += scan-tree.o 79OBJECTS += scan-tree.o
80OBJECTS += shared.o 80OBJECTS += shared.o
81OBJECTS += ui-atom.o 81OBJECTS += ui-atom.o
82OBJECTS += ui-blob.o 82OBJECTS += ui-blob.o
83OBJECTS += ui-clone.o 83OBJECTS += ui-clone.o
84OBJECTS += ui-commit.o 84OBJECTS += ui-commit.o
85OBJECTS += ui-diff.o 85OBJECTS += ui-diff.o
86OBJECTS += ui-log.o 86OBJECTS += ui-log.o
87OBJECTS += ui-patch.o 87OBJECTS += ui-patch.o
88OBJECTS += ui-plain.o 88OBJECTS += ui-plain.o
89OBJECTS += ui-refs.o 89OBJECTS += ui-refs.o
90OBJECTS += ui-repolist.o 90OBJECTS += ui-repolist.o
91OBJECTS += ui-shared.o 91OBJECTS += ui-shared.o
92OBJECTS += ui-snapshot.o 92OBJECTS += ui-snapshot.o
93OBJECTS += ui-stats.o
93OBJECTS += ui-summary.o 94OBJECTS += ui-summary.o
94OBJECTS += ui-tag.o 95OBJECTS += ui-tag.o
95OBJECTS += ui-tree.o 96OBJECTS += ui-tree.o
96 97
97ifdef NEEDS_LIBICONV 98ifdef NEEDS_LIBICONV
98 EXTLIBS += -liconv 99 EXTLIBS += -liconv
99endif 100endif
100 101
101 102
102.PHONY: all libgit test install uninstall clean force-version get-git 103.PHONY: all libgit test install uninstall clean force-version get-git
103 104
104all: cgit 105all: cgit
105 106
106VERSION: force-version 107VERSION: force-version
107 @./gen-version.sh "$(CGIT_VERSION)" 108 @./gen-version.sh "$(CGIT_VERSION)"
108-include VERSION 109-include VERSION
109 110
110 111
111CFLAGS += -g -Wall -Igit 112CFLAGS += -g -Wall -Igit
112CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)' 113CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)'
113CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"' 114CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"'
114CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"' 115CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"'
115CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"' 116CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"'
116CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"' 117CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"'
117 118
118ifdef NO_ICONV 119ifdef NO_ICONV
119 CFLAGS += -DNO_ICONV 120 CFLAGS += -DNO_ICONV
120endif 121endif
121ifdef NO_STRCASESTR 122ifdef NO_STRCASESTR
122 CFLAGS += -DNO_STRCASESTR 123 CFLAGS += -DNO_STRCASESTR
123endif 124endif
124 125
125cgit: $(OBJECTS) libgit 126cgit: $(OBJECTS) libgit
126 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS) 127 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS)
127 128
128cgit.o: VERSION 129cgit.o: VERSION
129 130
130-include $(OBJECTS:.o=.d) 131-include $(OBJECTS:.o=.d)
131 132
132libgit: 133libgit:
133 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 libgit.a 134 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 libgit.a
134 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 xdiff/lib.a 135 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 xdiff/lib.a
135 136
136test: all 137test: all
137 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all 138 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all
138 139
139install: all 140install: all
140 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH) 141 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH)
141 $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 142 $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
142 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH) 143 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH)
143 $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css 144 $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css
144 $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png 145 $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png
145 146
146uninstall: 147uninstall:
147 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 148 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
148 rm -f $(CGIT_DATA_PATH)/cgit.css 149 rm -f $(CGIT_DATA_PATH)/cgit.css
149 rm -f $(CGIT_DATA_PATH)/cgit.png 150 rm -f $(CGIT_DATA_PATH)/cgit.png
150 151
151clean: 152clean:
152 rm -f cgit VERSION *.o *.d 153 rm -f cgit VERSION *.o *.d
153 154
154get-git: 155get-git:
155 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 f35f605..608cab6 100644
--- a/cgit.c
+++ b/cgit.c
@@ -1,247 +1,255 @@
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);
81 else if (!strcmp(name, "summary-tags")) 84 else if (!strcmp(name, "summary-tags"))
82 ctx.cfg.summary_tags = atoi(value); 85 ctx.cfg.summary_tags = atoi(value);
83 else if (!strcmp(name, "agefile")) 86 else if (!strcmp(name, "agefile"))
84 ctx.cfg.agefile = xstrdup(value); 87 ctx.cfg.agefile = xstrdup(value);
85 else if (!strcmp(name, "renamelimit")) 88 else if (!strcmp(name, "renamelimit"))
86 ctx.cfg.renamelimit = atoi(value); 89 ctx.cfg.renamelimit = atoi(value);
87 else if (!strcmp(name, "robots")) 90 else if (!strcmp(name, "robots"))
88 ctx.cfg.robots = xstrdup(value); 91 ctx.cfg.robots = xstrdup(value);
89 else if (!strcmp(name, "clone-prefix")) 92 else if (!strcmp(name, "clone-prefix"))
90 ctx.cfg.clone_prefix = xstrdup(value); 93 ctx.cfg.clone_prefix = xstrdup(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,
208 int flags, void *cb_data) 216 int flags, void *cb_data)
209{ 217{
210 struct refmatch *info; 218 struct refmatch *info;
211 219
212 info = (struct refmatch *)cb_data; 220 info = (struct refmatch *)cb_data;
213 if (!strcmp(refname, info->req_ref)) 221 if (!strcmp(refname, info->req_ref))
214 info->match = 1; 222 info->match = 1;
215 if (!info->first_ref) 223 if (!info->first_ref)
216 info->first_ref = xstrdup(refname); 224 info->first_ref = xstrdup(refname);
217 return info->match; 225 return info->match;
218} 226}
219 227
220char *find_default_branch(struct cgit_repo *repo) 228char *find_default_branch(struct cgit_repo *repo)
221{ 229{
222 struct refmatch info; 230 struct refmatch info;
223 char *ref; 231 char *ref;
224 232
225 info.req_ref = repo->defbranch; 233 info.req_ref = repo->defbranch;
226 info.first_ref = NULL; 234 info.first_ref = NULL;
227 info.match = 0; 235 info.match = 0;
228 for_each_branch_ref(find_current_ref, &info); 236 for_each_branch_ref(find_current_ref, &info);
229 if (info.match) 237 if (info.match)
230 ref = info.req_ref; 238 ref = info.req_ref;
231 else 239 else
232 ref = info.first_ref; 240 ref = info.first_ref;
233 if (ref) 241 if (ref)
234 ref = xstrdup(ref); 242 ref = xstrdup(ref);
235 return ref; 243 return ref;
236} 244}
237 245
238static int prepare_repo_cmd(struct cgit_context *ctx) 246static int prepare_repo_cmd(struct cgit_context *ctx)
239{ 247{
240 char *tmp; 248 char *tmp;
241 unsigned char sha1[20]; 249 unsigned char sha1[20];
242 int nongit = 0; 250 int nongit = 0;
243 251
244 setenv("GIT_DIR", ctx->repo->path, 1); 252 setenv("GIT_DIR", ctx->repo->path, 1);
245 setup_git_directory_gently(&nongit); 253 setup_git_directory_gently(&nongit);
246 if (nongit) { 254 if (nongit) {
247 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title, 255 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title,
diff --git a/cgit.css b/cgit.css
index f19446d..e8214de 100644
--- a/cgit.css
+++ b/cgit.css
@@ -434,64 +434,140 @@ table.list td.sublevel-repo {
434div.pager { 434div.pager {
435 text-align: center; 435 text-align: center;
436 margin: 1em 0em 0em 0em; 436 margin: 1em 0em 0em 0em;
437} 437}
438 438
439div.pager a { 439div.pager a {
440 color: #777; 440 color: #777;
441 margin: 0em 0.5em; 441 margin: 0em 0.5em;
442} 442}
443 443
444span.age-mins { 444span.age-mins {
445 font-weight: bold; 445 font-weight: bold;
446 color: #080; 446 color: #080;
447} 447}
448 448
449span.age-hours { 449span.age-hours {
450 color: #080; 450 color: #080;
451} 451}
452 452
453span.age-days { 453span.age-days {
454 color: #040; 454 color: #040;
455} 455}
456 456
457span.age-weeks { 457span.age-weeks {
458 color: #444; 458 color: #444;
459} 459}
460 460
461span.age-months { 461span.age-months {
462 color: #888; 462 color: #888;
463} 463}
464 464
465span.age-years { 465span.age-years {
466 color: #bbb; 466 color: #bbb;
467} 467}
468div.footer { 468div.footer {
469 margin-top: 0.5em; 469 margin-top: 0.5em;
470 text-align: center; 470 text-align: center;
471 font-size: 80%; 471 font-size: 80%;
472 color: #ccc; 472 color: #ccc;
473} 473}
474a.branch-deco { 474a.branch-deco {
475 margin: 0px 0.5em; 475 margin: 0px 0.5em;
476 padding: 0px 0.25em; 476 padding: 0px 0.25em;
477 background-color: #88ff88; 477 background-color: #88ff88;
478 border: solid 1px #007700; 478 border: solid 1px #007700;
479} 479}
480a.tag-deco { 480a.tag-deco {
481 margin: 0px 0.5em; 481 margin: 0px 0.5em;
482 padding: 0px 0.25em; 482 padding: 0px 0.25em;
483 background-color: #ffff88; 483 background-color: #ffff88;
484 border: solid 1px #777700; 484 border: solid 1px #777700;
485} 485}
486a.remote-deco { 486a.remote-deco {
487 margin: 0px 0.5em; 487 margin: 0px 0.5em;
488 padding: 0px 0.25em; 488 padding: 0px 0.25em;
489 background-color: #ccccff; 489 background-color: #ccccff;
490 border: solid 1px #000077; 490 border: solid 1px #000077;
491} 491}
492a.deco { 492a.deco {
493 margin: 0px 0.5em; 493 margin: 0px 0.5em;
494 padding: 0px 0.25em; 494 padding: 0px 0.25em;
495 background-color: #ff8888; 495 background-color: #ff8888;
496 border: solid 1px #770000; 496 border: solid 1px #770000;
497} 497}
498table.stats {
499 border: solid 1px black;
500 border-collapse: collapse;
501}
502
503table.stats th {
504 text-align: left;
505 padding: 1px 0.5em;
506 background-color: #eee;
507 border: solid 1px black;
508}
509
510table.stats td {
511 text-align: right;
512 padding: 1px 0.5em;
513 border: solid 1px black;
514}
515
516table.stats td.total {
517 font-weight: bold;
518 text-align: left;
519}
520
521table.stats td.sum {
522 color: #c00;
523 font-weight: bold;
524 /*background-color: #eee; */
525}
526
527table.stats td.left {
528 text-align: left;
529}
530
531table.vgraph {
532 border-collapse: separate;
533 border: solid 1px black;
534 height: 200px;
535}
536
537table.vgraph th {
538 background-color: #eee;
539 font-weight: bold;
540 border: solid 1px white;
541 padding: 1px 0.5em;
542}
543
544table.vgraph td {
545 vertical-align: bottom;
546 padding: 0px 10px;
547}
548
549table.vgraph div.bar {
550 background-color: #eee;
551}
552
553table.hgraph {
554 border: solid 1px black;
555 width: 800px;
556}
557
558table.hgraph th {
559 background-color: #eee;
560 font-weight: bold;
561 border: solid 1px black;
562 padding: 1px 0.5em;
563}
564
565table.hgraph td {
566 vertical-align: center;
567 padding: 2px 2px;
568}
569
570table.hgraph div.bar {
571 background-color: #eee;
572 height: 1em;
573}
diff --git a/cgit.h b/cgit.h
index cb2f176..4fe94c6 100644
--- a/cgit.h
+++ b/cgit.h
@@ -1,226 +1,229 @@
1#ifndef CGIT_H 1#ifndef CGIT_H
2#define CGIT_H 2#define CGIT_H
3 3
4 4
5#include <git-compat-util.h> 5#include <git-compat-util.h>
6#include <cache.h> 6#include <cache.h>
7#include <grep.h> 7#include <grep.h>
8#include <object.h> 8#include <object.h>
9#include <tree.h> 9#include <tree.h>
10#include <commit.h> 10#include <commit.h>
11#include <tag.h> 11#include <tag.h>
12#include <diff.h> 12#include <diff.h>
13#include <diffcore.h> 13#include <diffcore.h>
14#include <refs.h> 14#include <refs.h>
15#include <revision.h> 15#include <revision.h>
16#include <log-tree.h> 16#include <log-tree.h>
17#include <archive.h> 17#include <archive.h>
18#include <xdiff/xdiff.h> 18#include <xdiff/xdiff.h>
19#include <utf8.h> 19#include <utf8.h>
20 20
21 21
22/* 22/*
23 * Dateformats used on misc. pages 23 * Dateformats used on misc. pages
24 */ 24 */
25#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)" 25#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)"
26#define FMT_SHORTDATE "%Y-%m-%d" 26#define FMT_SHORTDATE "%Y-%m-%d"
27#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ" 27#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ"
28 28
29 29
30/* 30/*
31 * Limits used for relative dates 31 * Limits used for relative dates
32 */ 32 */
33#define TM_MIN 60 33#define TM_MIN 60
34#define TM_HOUR (TM_MIN * 60) 34#define TM_HOUR (TM_MIN * 60)
35#define TM_DAY (TM_HOUR * 24) 35#define TM_DAY (TM_HOUR * 24)
36#define TM_WEEK (TM_DAY * 7) 36#define TM_WEEK (TM_DAY * 7)
37#define TM_YEAR (TM_DAY * 365) 37#define TM_YEAR (TM_DAY * 365)
38#define TM_MONTH (TM_YEAR / 12.0) 38#define TM_MONTH (TM_YEAR / 12.0)
39 39
40 40
41/* 41/*
42 * Default encoding 42 * Default encoding
43 */ 43 */
44#define PAGE_ENCODING "UTF-8" 44#define PAGE_ENCODING "UTF-8"
45 45
46typedef void (*configfn)(const char *name, const char *value); 46typedef void (*configfn)(const char *name, const char *value);
47typedef void (*filepair_fn)(struct diff_filepair *pair); 47typedef void (*filepair_fn)(struct diff_filepair *pair);
48typedef void (*linediff_fn)(char *line, int len); 48typedef void (*linediff_fn)(char *line, int len);
49 49
50struct cgit_repo { 50struct cgit_repo {
51 char *url; 51 char *url;
52 char *name; 52 char *name;
53 char *path; 53 char *path;
54 char *desc; 54 char *desc;
55 char *owner; 55 char *owner;
56 char *defbranch; 56 char *defbranch;
57 char *group; 57 char *group;
58 char *module_link; 58 char *module_link;
59 char *readme; 59 char *readme;
60 char *clone_url; 60 char *clone_url;
61 int snapshots; 61 int snapshots;
62 int enable_log_filecount; 62 int enable_log_filecount;
63 int enable_log_linecount; 63 int enable_log_linecount;
64 int 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;
88 char *tagger_email; 89 char *tagger_email;
89 unsigned long tagger_date; 90 unsigned long tagger_date;
90 char *msg; 91 char *msg;
91}; 92};
92 93
93struct refinfo { 94struct refinfo {
94 const char *refname; 95 const char *refname;
95 struct object *object; 96 struct object *object;
96 union { 97 union {
97 struct taginfo *tag; 98 struct taginfo *tag;
98 struct commitinfo *commit; 99 struct commitinfo *commit;
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};
187 190
188struct cgit_snapshot_format { 191struct cgit_snapshot_format {
189 const char *suffix; 192 const char *suffix;
190 const char *mimetype; 193 const char *mimetype;
191 write_archive_fn_t write_func; 194 write_archive_fn_t write_func;
192 int bit; 195 int bit;
193}; 196};
194 197
195extern const char *cgit_version; 198extern const char *cgit_version;
196 199
197extern struct cgit_repolist cgit_repolist; 200extern struct cgit_repolist cgit_repolist;
198extern struct cgit_context ctx; 201extern struct cgit_context ctx;
199extern const struct cgit_snapshot_format cgit_snapshot_formats[]; 202extern const struct cgit_snapshot_format cgit_snapshot_formats[];
200 203
201extern struct cgit_repo *cgit_add_repo(const char *url); 204extern struct cgit_repo *cgit_add_repo(const char *url);
202extern struct cgit_repo *cgit_get_repoinfo(const char *url); 205extern struct cgit_repo *cgit_get_repoinfo(const char *url);
203extern void cgit_repo_config_cb(const char *name, const char *value); 206extern void cgit_repo_config_cb(const char *name, const char *value);
204 207
205extern int chk_zero(int result, char *msg); 208extern int chk_zero(int result, char *msg);
206extern int chk_positive(int result, char *msg); 209extern int chk_positive(int result, char *msg);
207extern int chk_non_negative(int result, char *msg); 210extern int chk_non_negative(int result, char *msg);
208 211
209extern char *trim_end(const char *str, char c); 212extern char *trim_end(const char *str, char c);
210extern char *strlpart(char *txt, int maxlen); 213extern char *strlpart(char *txt, int maxlen);
211extern char *strrpart(char *txt, int maxlen); 214extern char *strrpart(char *txt, int maxlen);
212 215
213extern void cgit_add_ref(struct reflist *list, struct refinfo *ref); 216extern void cgit_add_ref(struct reflist *list, struct refinfo *ref);
214extern int cgit_refs_cb(const char *refname, const unsigned char *sha1, 217extern int cgit_refs_cb(const char *refname, const unsigned char *sha1,
215 int flags, void *cb_data); 218 int flags, void *cb_data);
216 219
217extern void *cgit_free_commitinfo(struct commitinfo *info); 220extern void *cgit_free_commitinfo(struct commitinfo *info);
218 221
219extern int cgit_diff_files(const unsigned char *old_sha1, 222extern int cgit_diff_files(const unsigned char *old_sha1,
220 const unsigned char *new_sha1, 223 const unsigned char *new_sha1,
221 linediff_fn fn); 224 linediff_fn fn);
222 225
223extern void cgit_diff_tree(const unsigned char *old_sha1, 226extern void cgit_diff_tree(const unsigned char *old_sha1,
224 const unsigned char *new_sha1, 227 const unsigned char *new_sha1,
225 filepair_fn fn, const char *prefix); 228 filepair_fn fn, const char *prefix);
226 229
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index ab9ab66..09f56a6 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -68,300 +68,317 @@ enable-log-filecount
68 Flag which, when set to "1", will make cgit print the number of 68 Flag which, when set to "1", will make cgit print the number of
69 modified files for each commit on the repository log page. Default 69 modified files for each commit on the repository log page. Default
70 value: "0". 70 value: "0".
71 71
72enable-log-linecount 72enable-log-linecount
73 Flag which, when set to "1", will make cgit print the number of added 73 Flag which, when set to "1", will make cgit print the number of added
74 and removed lines for each commit on the repository log page. Default 74 and removed lines for each commit on the repository log page. Default
75 value: "0". 75 value: "0".
76 76
77favicon 77favicon
78 Url used as link to a shortcut icon for cgit. If specified, it is 78 Url used as link to a shortcut icon for cgit. If specified, it is
79 suggested to use the value "/favicon.ico" since certain browsers will 79 suggested to use the value "/favicon.ico" since certain browsers will
80 ignore other values. Default value: none. 80 ignore other values. Default value: none.
81 81
82footer 82footer
83 The content of the file specified with this option will be included 83 The content of the file specified with this option will be included
84 verbatim at the bottom of all pages (i.e. it replaces the standard 84 verbatim at the bottom of all pages (i.e. it replaces the standard
85 "generated by..." message. Default value: none. 85 "generated by..." message. Default value: none.
86 86
87include 87include
88 Name of a configfile to include before the rest of the current config- 88 Name of a configfile to include before the rest of the current config-
89 file is parsed. Default value: none. 89 file is parsed. Default value: none.
90 90
91index-header 91index-header
92 The content of the file specified with this option will be included 92 The content of the file specified with this option will be included
93 verbatim above the repository index. This setting is deprecated, and 93 verbatim above the repository index. This setting is deprecated, and
94 will not be supported by cgit-1.0 (use root-readme instead). Default 94 will not be supported by cgit-1.0 (use root-readme instead). Default
95 value: none. 95 value: none.
96 96
97index-info 97index-info
98 The content of the file specified with this option will be included 98 The content of the file specified with this option will be included
99 verbatim below the heading on the repository index page. This setting 99 verbatim below the heading on the repository index page. This setting
100 is deprecated, and will not be supported by cgit-1.0 (use root-desc 100 is deprecated, and will not be supported by cgit-1.0 (use root-desc
101 instead). Default value: none. 101 instead). Default value: none.
102 102
103local-time 103local-time
104 Flag which, if set to "1", makes cgit print commit and tag times in the 104 Flag which, if set to "1", makes cgit print commit and tag times in the
105 servers timezone. Default value: "0". 105 servers timezone. Default value: "0".
106 106
107logo 107logo
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
156root-desc 161root-desc
157 Text printed below the heading on the repository index page. Default 162 Text printed below the heading on the repository index page. Default
158 value: "a fast webinterface for the git dscm". 163 value: "a fast webinterface for the git dscm".
159 164
160root-readme: 165root-readme:
161 The content of the file specified with this option will be included 166 The content of the file specified with this option will be included
162 verbatim below the "about" link on the repository index page. Default 167 verbatim below the "about" link on the repository index page. Default
163 value: none. 168 value: none.
164 169
165root-title 170root-title
166 Text printed as heading on the repository index page. Default value: 171 Text printed as heading on the repository index page. Default value:
167 "Git Repository Browser". 172 "Git Repository Browser".
168 173
169snapshots 174snapshots
170 Text which specifies the default (and allowed) set of snapshot formats 175 Text which specifies the default (and allowed) set of snapshot formats
171 supported by cgit. The value is a space-separated list of zero or more 176 supported by cgit. The value is a space-separated list of zero or more
172 of the following values: 177 of the following values:
173 "tar" uncompressed tar-file 178 "tar" uncompressed tar-file
174 "tar.gz"gzip-compressed tar-file 179 "tar.gz"gzip-compressed tar-file
175 "tar.bz2"bzip-compressed tar-file 180 "tar.bz2"bzip-compressed tar-file
176 "zip" zip-file 181 "zip" zip-file
177 Default value: none. 182 Default value: none.
178 183
179summary-branches 184summary-branches
180 Specifies the number of branches to display in the repository "summary" 185 Specifies the number of branches to display in the repository "summary"
181 view. Default value: "10". 186 view. Default value: "10".
182 187
183summary-log 188summary-log
184 Specifies the number of log entries to display in the repository 189 Specifies the number of log entries to display in the repository
185 "summary" view. Default value: "10". 190 "summary" view. Default value: "10".
186 191
187summary-tags 192summary-tags
188 Specifies the number of tags to display in the repository "summary" 193 Specifies the number of tags to display in the repository "summary"
189 view. Default value: "10". 194 view. Default value: "10".
190 195
191virtual-root 196virtual-root
192 Url which, if specified, will be used as root for all cgit links. It 197 Url which, if specified, will be used as root for all cgit links. It
193 will also cause cgit to generate 'virtual urls', i.e. urls like 198 will also cause cgit to generate 'virtual urls', i.e. urls like
194 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default 199 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default
195 value: none. 200 value: none.
196 NOTE: cgit has recently learned how to use PATH_INFO to achieve the 201 NOTE: cgit has recently learned how to use PATH_INFO to achieve the
197 same kind of virtual urls, so this option will probably be deprecated. 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
245EXAMPLE CGITRC FILE 255EXAMPLE CGITRC FILE
246------------------- 256-------------------
247 257
248# Enable caching of up to 1000 output entriess 258# Enable caching of up to 1000 output entriess
249cache-size=1000 259cache-size=1000
250 260
251 261
252# Specify some default clone prefixes 262# Specify some default clone prefixes
253clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git 263clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git
254 264
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.bz2 and zip-files 305# Allow download of tar.gz, tar.bz2 and zip-files
292snapshots=tar.gz tar.bz2 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
317 331
318 332
319# The next repositories will be displayed under the 'extras' heading 333# The next repositories will be displayed under the 'extras' heading
320repo.group=extras 334repo.group=extras
321 335
322 336
323repo.url=baz 337repo.url=baz
324repo.path=/pub/git/baz.git 338repo.path=/pub/git/baz.git
325repo.desc=a set of extensions for bar users 339repo.desc=a set of extensions for bar users
326 340
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,164 +1,171 @@
1/* cmd.c: the cgit command dispatcher 1/* cmd.c: the cgit command dispatcher
2 * 2 *
3 * Copyright (C) 2008 Lars Hjemli 3 * Copyright (C) 2008 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "cmd.h" 10#include "cmd.h"
11#include "cache.h" 11#include "cache.h"
12#include "ui-shared.h" 12#include "ui-shared.h"
13#include "ui-atom.h" 13#include "ui-atom.h"
14#include "ui-blob.h" 14#include "ui-blob.h"
15#include "ui-clone.h" 15#include "ui-clone.h"
16#include "ui-commit.h" 16#include "ui-commit.h"
17#include "ui-diff.h" 17#include "ui-diff.h"
18#include "ui-log.h" 18#include "ui-log.h"
19#include "ui-patch.h" 19#include "ui-patch.h"
20#include "ui-plain.h" 20#include "ui-plain.h"
21#include "ui-refs.h" 21#include "ui-refs.h"
22#include "ui-repolist.h" 22#include "ui-repolist.h"
23#include "ui-snapshot.h" 23#include "ui-snapshot.h"
24#include "ui-stats.h"
24#include "ui-summary.h" 25#include "ui-summary.h"
25#include "ui-tag.h" 26#include "ui-tag.h"
26#include "ui-tree.h" 27#include "ui-tree.h"
27 28
28static void HEAD_fn(struct cgit_context *ctx) 29static void HEAD_fn(struct cgit_context *ctx)
29{ 30{
30 cgit_clone_head(ctx); 31 cgit_clone_head(ctx);
31} 32}
32 33
33static void atom_fn(struct cgit_context *ctx) 34static void atom_fn(struct cgit_context *ctx)
34{ 35{
35 cgit_print_atom(ctx->qry.head, ctx->qry.path, 10); 36 cgit_print_atom(ctx->qry.head, ctx->qry.path, 10);
36} 37}
37 38
38static void about_fn(struct cgit_context *ctx) 39static void about_fn(struct cgit_context *ctx)
39{ 40{
40 if (ctx->repo) 41 if (ctx->repo)
41 cgit_print_repo_readme(); 42 cgit_print_repo_readme();
42 else 43 else
43 cgit_print_site_readme(); 44 cgit_print_site_readme();
44} 45}
45 46
46static void blob_fn(struct cgit_context *ctx) 47static void blob_fn(struct cgit_context *ctx)
47{ 48{
48 cgit_print_blob(ctx->qry.sha1, ctx->qry.path, ctx->qry.head); 49 cgit_print_blob(ctx->qry.sha1, ctx->qry.path, ctx->qry.head);
49} 50}
50 51
51static void commit_fn(struct cgit_context *ctx) 52static void commit_fn(struct cgit_context *ctx)
52{ 53{
53 cgit_print_commit(ctx->qry.sha1); 54 cgit_print_commit(ctx->qry.sha1);
54} 55}
55 56
56static void diff_fn(struct cgit_context *ctx) 57static void diff_fn(struct cgit_context *ctx)
57{ 58{
58 cgit_print_diff(ctx->qry.sha1, ctx->qry.sha2, ctx->qry.path); 59 cgit_print_diff(ctx->qry.sha1, ctx->qry.sha2, ctx->qry.path);
59} 60}
60 61
61static void info_fn(struct cgit_context *ctx) 62static void info_fn(struct cgit_context *ctx)
62{ 63{
63 cgit_clone_info(ctx); 64 cgit_clone_info(ctx);
64} 65}
65 66
66static void log_fn(struct cgit_context *ctx) 67static void log_fn(struct cgit_context *ctx)
67{ 68{
68 cgit_print_log(ctx->qry.sha1, ctx->qry.ofs, ctx->cfg.max_commit_count, 69 cgit_print_log(ctx->qry.sha1, ctx->qry.ofs, ctx->cfg.max_commit_count,
69 ctx->qry.grep, ctx->qry.search, ctx->qry.path, 1); 70 ctx->qry.grep, ctx->qry.search, ctx->qry.path, 1);
70} 71}
71 72
72static void ls_cache_fn(struct cgit_context *ctx) 73static void ls_cache_fn(struct cgit_context *ctx)
73{ 74{
74 ctx->page.mimetype = "text/plain"; 75 ctx->page.mimetype = "text/plain";
75 ctx->page.filename = "ls-cache.txt"; 76 ctx->page.filename = "ls-cache.txt";
76 cgit_print_http_headers(ctx); 77 cgit_print_http_headers(ctx);
77 cache_ls(ctx->cfg.cache_root); 78 cache_ls(ctx->cfg.cache_root);
78} 79}
79 80
80static void objects_fn(struct cgit_context *ctx) 81static void objects_fn(struct cgit_context *ctx)
81{ 82{
82 cgit_clone_objects(ctx); 83 cgit_clone_objects(ctx);
83} 84}
84 85
85static void repolist_fn(struct cgit_context *ctx) 86static void repolist_fn(struct cgit_context *ctx)
86{ 87{
87 cgit_print_repolist(); 88 cgit_print_repolist();
88} 89}
89 90
90static void patch_fn(struct cgit_context *ctx) 91static void patch_fn(struct cgit_context *ctx)
91{ 92{
92 cgit_print_patch(ctx->qry.sha1); 93 cgit_print_patch(ctx->qry.sha1);
93} 94}
94 95
95static void plain_fn(struct cgit_context *ctx) 96static void plain_fn(struct cgit_context *ctx)
96{ 97{
97 cgit_print_plain(ctx); 98 cgit_print_plain(ctx);
98} 99}
99 100
100static void refs_fn(struct cgit_context *ctx) 101static void refs_fn(struct cgit_context *ctx)
101{ 102{
102 cgit_print_refs(); 103 cgit_print_refs();
103} 104}
104 105
105static void snapshot_fn(struct cgit_context *ctx) 106static void snapshot_fn(struct cgit_context *ctx)
106{ 107{
107 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, 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
@@ -1,124 +1,125 @@
1/* shared.c: global vars + some callback functions 1/* shared.c: global vars + some callback functions
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10 10
11struct cgit_repolist cgit_repolist; 11struct cgit_repolist cgit_repolist;
12struct cgit_context ctx; 12struct cgit_context ctx;
13int cgit_cmd; 13int cgit_cmd;
14 14
15int chk_zero(int result, char *msg) 15int chk_zero(int result, char *msg)
16{ 16{
17 if (result != 0) 17 if (result != 0)
18 die("%s: %s", msg, strerror(errno)); 18 die("%s: %s", msg, strerror(errno));
19 return result; 19 return result;
20} 20}
21 21
22int chk_positive(int result, char *msg) 22int chk_positive(int result, char *msg)
23{ 23{
24 if (result <= 0) 24 if (result <= 0)
25 die("%s: %s", msg, strerror(errno)); 25 die("%s: %s", msg, strerror(errno));
26 return result; 26 return result;
27} 27}
28 28
29int chk_non_negative(int result, char *msg) 29int chk_non_negative(int result, char *msg)
30{ 30{
31 if (result < 0) 31 if (result < 0)
32 die("%s: %s",msg, strerror(errno)); 32 die("%s: %s",msg, strerror(errno));
33 return result; 33 return result;
34} 34}
35 35
36struct cgit_repo *cgit_add_repo(const char *url) 36struct cgit_repo *cgit_add_repo(const char *url)
37{ 37{
38 struct cgit_repo *ret; 38 struct cgit_repo *ret;
39 39
40 if (++cgit_repolist.count > cgit_repolist.length) { 40 if (++cgit_repolist.count > cgit_repolist.length) {
41 if (cgit_repolist.length == 0) 41 if (cgit_repolist.length == 0)
42 cgit_repolist.length = 8; 42 cgit_repolist.length = 8;
43 else 43 else
44 cgit_repolist.length *= 2; 44 cgit_repolist.length *= 2;
45 cgit_repolist.repos = xrealloc(cgit_repolist.repos, 45 cgit_repolist.repos = xrealloc(cgit_repolist.repos,
46 cgit_repolist.length * 46 cgit_repolist.length *
47 sizeof(struct cgit_repo)); 47 sizeof(struct cgit_repo));
48 } 48 }
49 49
50 ret = &cgit_repolist.repos[cgit_repolist.count-1]; 50 ret = &cgit_repolist.repos[cgit_repolist.count-1];
51 ret->url = trim_end(url, '/'); 51 ret->url = trim_end(url, '/');
52 ret->name = ret->url; 52 ret->name = ret->url;
53 ret->path = NULL; 53 ret->path = NULL;
54 ret->desc = "[no description]"; 54 ret->desc = "[no description]";
55 ret->owner = NULL; 55 ret->owner = NULL;
56 ret->group = ctx.cfg.repo_group; 56 ret->group = ctx.cfg.repo_group;
57 ret->defbranch = "master"; 57 ret->defbranch = "master";
58 ret->snapshots = ctx.cfg.snapshots; 58 ret->snapshots = ctx.cfg.snapshots;
59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount; 59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount; 60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
61 ret->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);
85 free(info->committer_email); 86 free(info->committer_email);
86 free(info->subject); 87 free(info->subject);
87 free(info->msg); 88 free(info->msg);
88 free(info->msg_encoding); 89 free(info->msg_encoding);
89 free(info); 90 free(info);
90 return NULL; 91 return NULL;
91} 92}
92 93
93char *trim_end(const char *str, char c) 94char *trim_end(const char *str, char c)
94{ 95{
95 int len; 96 int len;
96 char *s, *t; 97 char *s, *t;
97 98
98 if (str == NULL) 99 if (str == NULL)
99 return NULL; 100 return NULL;
100 t = (char *)str; 101 t = (char *)str;
101 len = strlen(t); 102 len = strlen(t);
102 while(len > 0 && t[len - 1] == c) 103 while(len > 0 && t[len - 1] == c)
103 len--; 104 len--;
104 105
105 if (len == 0) 106 if (len == 0)
106 return NULL; 107 return NULL;
107 108
108 c = t[len]; 109 c = t[len];
109 t[len] = '\0'; 110 t[len] = '\0';
110 s = xstrdup(t); 111 s = xstrdup(t);
111 t[len] = c; 112 t[len] = c;
112 return s; 113 return s;
113} 114}
114 115
115char *strlpart(char *txt, int maxlen) 116char *strlpart(char *txt, int maxlen)
116{ 117{
117 char *result; 118 char *result;
118 119
119 if (!txt) 120 if (!txt)
120 return txt; 121 return txt;
121 122
122 if (strlen(txt) <= maxlen) 123 if (strlen(txt) <= maxlen)
123 return txt; 124 return txt;
124 result = xmalloc(maxlen + 1); 125 result = xmalloc(maxlen + 1);
diff --git a/ui-shared.c b/ui-shared.c
index fba1ba6..4f28512 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -308,128 +308,134 @@ void cgit_log_link(char *name, char *title, char *class, char *head,
308 htmlf("%d", ofs); 308 htmlf("%d", ofs);
309 delim = "&"; 309 delim = "&";
310 } 310 }
311 if (showmsg) { 311 if (showmsg) {
312 html(delim); 312 html(delim);
313 html("showmsg=1"); 313 html("showmsg=1");
314 } 314 }
315 html("'>"); 315 html("'>");
316 html_txt(name); 316 html_txt(name);
317 html("</a>"); 317 html("</a>");
318} 318}
319 319
320void cgit_commit_link(char *name, char *title, char *class, char *head, 320void cgit_commit_link(char *name, char *title, char *class, char *head,
321 char *rev) 321 char *rev)
322{ 322{
323 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) { 323 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
324 name[ctx.cfg.max_msg_len] = '\0'; 324 name[ctx.cfg.max_msg_len] = '\0';
325 name[ctx.cfg.max_msg_len - 1] = '.'; 325 name[ctx.cfg.max_msg_len - 1] = '.';
326 name[ctx.cfg.max_msg_len - 2] = '.'; 326 name[ctx.cfg.max_msg_len - 2] = '.';
327 name[ctx.cfg.max_msg_len - 3] = '.'; 327 name[ctx.cfg.max_msg_len - 3] = '.';
328 } 328 }
329 reporevlink("commit", name, title, class, head, rev, NULL); 329 reporevlink("commit", name, title, class, head, rev, NULL);
330} 330}
331 331
332void cgit_refs_link(char *name, char *title, char *class, char *head, 332void cgit_refs_link(char *name, char *title, char *class, char *head,
333 char *rev, char *path) 333 char *rev, char *path)
334{ 334{
335 reporevlink("refs", name, title, class, head, rev, path); 335 reporevlink("refs", name, title, class, head, rev, path);
336} 336}
337 337
338void cgit_snapshot_link(char *name, char *title, char *class, char *head, 338void cgit_snapshot_link(char *name, char *title, char *class, char *head,
339 char *rev, char *archivename) 339 char *rev, char *archivename)
340{ 340{
341 reporevlink("snapshot", name, title, class, head, rev, archivename); 341 reporevlink("snapshot", name, title, class, head, rev, archivename);
342} 342}
343 343
344void cgit_diff_link(char *name, char *title, char *class, char *head, 344void cgit_diff_link(char *name, char *title, char *class, char *head,
345 char *new_rev, char *old_rev, char *path) 345 char *new_rev, char *old_rev, char *path)
346{ 346{
347 char *delim; 347 char *delim;
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];
396 struct tm *time; 402 struct tm *time;
397 403
398 if (!secs) 404 if (!secs)
399 return; 405 return;
400 if(local_time) 406 if(local_time)
401 time = localtime(&secs); 407 time = localtime(&secs);
402 else 408 else
403 time = gmtime(&secs); 409 time = gmtime(&secs);
404 strftime(buf, sizeof(buf)-1, format, time); 410 strftime(buf, sizeof(buf)-1, format, time);
405 html_txt(buf); 411 html_txt(buf);
406} 412}
407 413
408void cgit_print_age(time_t t, time_t max_relative, char *format) 414void cgit_print_age(time_t t, time_t max_relative, char *format)
409{ 415{
410 time_t now, secs; 416 time_t now, secs;
411 417
412 if (!t) 418 if (!t)
413 return; 419 return;
414 time(&now); 420 time(&now);
415 secs = now - t; 421 secs = now - t;
416 422
417 if (secs > max_relative && max_relative >= 0) { 423 if (secs > max_relative && max_relative >= 0) {
418 cgit_print_date(t, format, ctx.cfg.local_time); 424 cgit_print_date(t, format, ctx.cfg.local_time);
419 return; 425 return;
420 } 426 }
421 427
422 if (secs < TM_HOUR * 2) { 428 if (secs < TM_HOUR * 2) {
423 htmlf("<span class='age-mins'>%.0f min.</span>", 429 htmlf("<span class='age-mins'>%.0f min.</span>",
424 secs * 1.0 / TM_MIN); 430 secs * 1.0 / TM_MIN);
425 return; 431 return;
426 } 432 }
427 if (secs < TM_DAY * 2) { 433 if (secs < TM_DAY * 2) {
428 htmlf("<span class='age-hours'>%.0f hours</span>", 434 htmlf("<span class='age-hours'>%.0f hours</span>",
429 secs * 1.0 / TM_HOUR); 435 secs * 1.0 / TM_HOUR);
430 return; 436 return;
431 } 437 }
432 if (secs < TM_WEEK * 2) { 438 if (secs < TM_WEEK * 2) {
433 htmlf("<span class='age-days'>%.0f days</span>", 439 htmlf("<span class='age-days'>%.0f days</span>",
434 secs * 1.0 / TM_DAY); 440 secs * 1.0 / TM_DAY);
435 return; 441 return;
@@ -496,233 +502,236 @@ void cgit_print_docstart(struct cgit_context *ctx)
496 html("<body>\n"); 502 html("<body>\n");
497} 503}
498 504
499void cgit_print_docend() 505void cgit_print_docend()
500{ 506{
501 html("</div>"); 507 html("</div>");
502 if (ctx.cfg.footer) 508 if (ctx.cfg.footer)
503 html_include(ctx.cfg.footer); 509 html_include(ctx.cfg.footer);
504 else { 510 else {
505 htmlf("<div class='footer'>generated by cgit %s at ", 511 htmlf("<div class='footer'>generated by cgit %s at ",
506 cgit_version); 512 cgit_version);
507 cgit_print_date(time(NULL), FMT_LONGDATE, ctx.cfg.local_time); 513 cgit_print_date(time(NULL), FMT_LONGDATE, ctx.cfg.local_time);
508 html("</div>\n"); 514 html("</div>\n");
509 } 515 }
510 html("</body>\n</html>\n"); 516 html("</body>\n</html>\n");
511} 517}
512 518
513int print_branch_option(const char *refname, const unsigned char *sha1, 519int print_branch_option(const char *refname, const unsigned char *sha1,
514 int flags, void *cb_data) 520 int flags, void *cb_data)
515{ 521{
516 char *name = (char *)refname; 522 char *name = (char *)refname;
517 html_option(name, name, ctx.qry.head); 523 html_option(name, name, ctx.qry.head);
518 return 0; 524 return 0;
519} 525}
520 526
521int print_archive_ref(const char *refname, const unsigned char *sha1, 527int print_archive_ref(const char *refname, const unsigned char *sha1,
522 int flags, void *cb_data) 528 int flags, void *cb_data)
523{ 529{
524 struct tag *tag; 530 struct tag *tag;
525 struct taginfo *info; 531 struct taginfo *info;
526 struct object *obj; 532 struct object *obj;
527 char buf[256], *url; 533 char buf[256], *url;
528 unsigned char fileid[20]; 534 unsigned char fileid[20];
529 int *header = (int *)cb_data; 535 int *header = (int *)cb_data;
530 536
531 if (prefixcmp(refname, "refs/archives")) 537 if (prefixcmp(refname, "refs/archives"))
532 return 0; 538 return 0;
533 strncpy(buf, refname+14, sizeof(buf)); 539 strncpy(buf, refname+14, sizeof(buf));
534 obj = parse_object(sha1); 540 obj = parse_object(sha1);
535 if (!obj) 541 if (!obj)
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
590const char *fallback_cmd = "repolist"; 596const char *fallback_cmd = "repolist";
591 597
592char *hc(struct cgit_cmd *cmd, const char *page) 598char *hc(struct cgit_cmd *cmd, const char *page)
593{ 599{
594 return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active"); 600 return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active");
595} 601}
596 602
597void cgit_print_pageheader(struct cgit_context *ctx) 603void cgit_print_pageheader(struct cgit_context *ctx)
598{ 604{
599 struct cgit_cmd *cmd = cgit_get_cmd(ctx); 605 struct cgit_cmd *cmd = cgit_get_cmd(ctx);
600 606
601 if (!cmd && ctx->repo) 607 if (!cmd && ctx->repo)
602 fallback_cmd = "summary"; 608 fallback_cmd = "summary";
603 609
604 html("<table id='header'>\n"); 610 html("<table id='header'>\n");
605 html("<tr>\n"); 611 html("<tr>\n");
606 html("<td class='logo' rowspan='2'><a href='"); 612 html("<td class='logo' rowspan='2'><a href='");
607 if (ctx->cfg.logo_link) 613 if (ctx->cfg.logo_link)
608 html_attr(ctx->cfg.logo_link); 614 html_attr(ctx->cfg.logo_link);
609 else 615 else
610 html_attr(cgit_rooturl()); 616 html_attr(cgit_rooturl());
611 html("'><img src='"); 617 html("'><img src='");
612 html_attr(ctx->cfg.logo); 618 html_attr(ctx->cfg.logo);
613 html("' alt='cgit logo'/></a></td>\n"); 619 html("' alt='cgit logo'/></a></td>\n");
614 620
615 html("<td class='main'>"); 621 html("<td class='main'>");
616 if (ctx->repo) { 622 if (ctx->repo) {
617 cgit_index_link("index", NULL, NULL, NULL, 0); 623 cgit_index_link("index", NULL, NULL, NULL, 0);
618 html(" : "); 624 html(" : ");
619 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); 625 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
620 html("</td><td class='form'>"); 626 html("</td><td class='form'>");
621 html("<form method='get' action=''>\n"); 627 html("<form method='get' action=''>\n");
622 add_hidden_formfields(0, 1, ctx->qry.page); 628 cgit_add_hidden_formfields(0, 1, ctx->qry.page);
623 html("<select name='h' onchange='this.form.submit();'>\n"); 629 html("<select name='h' onchange='this.form.submit();'>\n");
624 for_each_branch_ref(print_branch_option, ctx->qry.head); 630 for_each_branch_ref(print_branch_option, ctx->qry.head);
625 html("</select> "); 631 html("</select> ");
626 html("<input type='submit' name='' value='switch'/>"); 632 html("<input type='submit' name='' value='switch'/>");
627 html("</form>"); 633 html("</form>");
628 } else 634 } else
629 html_txt(ctx->cfg.root_title); 635 html_txt(ctx->cfg.root_title);
630 html("</td></tr>\n"); 636 html("</td></tr>\n");
631 637
632 html("<tr><td class='sub'>"); 638 html("<tr><td class='sub'>");
633 if (ctx->repo) { 639 if (ctx->repo) {
634 html_txt(ctx->repo->desc); 640 html_txt(ctx->repo->desc);
635 html("</td><td class='sub right'>"); 641 html("</td><td class='sub right'>");
636 html_txt(ctx->repo->owner); 642 html_txt(ctx->repo->owner);
637 } else { 643 } else {
638 if (ctx->cfg.root_desc) 644 if (ctx->cfg.root_desc)
639 html_txt(ctx->cfg.root_desc); 645 html_txt(ctx->cfg.root_desc);
640 else if (ctx->cfg.index_info) 646 else if (ctx->cfg.index_info)
641 html_include(ctx->cfg.index_info); 647 html_include(ctx->cfg.index_info);
642 } 648 }
643 html("</td></tr></table>\n"); 649 html("</td></tr></table>\n");
644 650
645 html("<table class='tabs'><tr><td>\n"); 651 html("<table class='tabs'><tr><td>\n");
646 if (ctx->repo) { 652 if (ctx->repo) {
647 cgit_summary_link("summary", NULL, hc(cmd, "summary"), 653 cgit_summary_link("summary", NULL, hc(cmd, "summary"),
648 ctx->qry.head); 654 ctx->qry.head);
649 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head, 655 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head,
650 ctx->qry.sha1, NULL); 656 ctx->qry.sha1, NULL);
651 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head, 657 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head,
652 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg); 658 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg);
653 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 659 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
654 ctx->qry.sha1, NULL); 660 ctx->qry.sha1, NULL);
655 cgit_commit_link("commit", NULL, hc(cmd, "commit"), 661 cgit_commit_link("commit", NULL, hc(cmd, "commit"),
656 ctx->qry.head, ctx->qry.sha1); 662 ctx->qry.head, ctx->qry.sha1);
657 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 663 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
658 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);
659 if (ctx->repo->readme) 668 if (ctx->repo->readme)
660 reporevlink("about", "about", NULL, 669 reporevlink("about", "about", NULL,
661 hc(cmd, "about"), ctx->qry.head, NULL, 670 hc(cmd, "about"), ctx->qry.head, NULL,
662 NULL); 671 NULL);
663 html("</td><td class='form'>"); 672 html("</td><td class='form'>");
664 html("<form class='right' method='get' action='"); 673 html("<form class='right' method='get' action='");
665 if (ctx->cfg.virtual_root) 674 if (ctx->cfg.virtual_root)
666 html_url_path(cgit_fileurl(ctx->qry.repo, "log", 675 html_url_path(cgit_fileurl(ctx->qry.repo, "log",
667 ctx->qry.path, NULL)); 676 ctx->qry.path, NULL));
668 html("'>\n"); 677 html("'>\n");
669 add_hidden_formfields(1, 0, "log"); 678 cgit_add_hidden_formfields(1, 0, "log");
670 html("<select name='qt'>\n"); 679 html("<select name='qt'>\n");
671 html_option("grep", "log msg", ctx->qry.grep); 680 html_option("grep", "log msg", ctx->qry.grep);
672 html_option("author", "author", ctx->qry.grep); 681 html_option("author", "author", ctx->qry.grep);
673 html_option("committer", "committer", ctx->qry.grep); 682 html_option("committer", "committer", ctx->qry.grep);
674 html("</select>\n"); 683 html("</select>\n");
675 html("<input class='txt' type='text' size='10' name='q' value='"); 684 html("<input class='txt' type='text' size='10' name='q' value='");
676 html_attr(ctx->qry.search); 685 html_attr(ctx->qry.search);
677 html("'/>\n"); 686 html("'/>\n");
678 html("<input type='submit' value='search'/>\n"); 687 html("<input type='submit' value='search'/>\n");
679 html("</form>\n"); 688 html("</form>\n");
680 } else { 689 } else {
681 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0); 690 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0);
682 if (ctx->cfg.root_readme) 691 if (ctx->cfg.root_readme)
683 site_link("about", "about", NULL, hc(cmd, "about"), 692 site_link("about", "about", NULL, hc(cmd, "about"),
684 NULL, 0); 693 NULL, 0);
685 html("</td><td class='form'>"); 694 html("</td><td class='form'>");
686 html("<form method='get' action='"); 695 html("<form method='get' action='");
687 html_attr(cgit_rooturl()); 696 html_attr(cgit_rooturl());
688 html("'>\n"); 697 html("'>\n");
689 html("<input type='text' name='q' size='10' value='"); 698 html("<input type='text' name='q' size='10' value='");
690 html_attr(ctx->qry.search); 699 html_attr(ctx->qry.search);
691 html("'/>\n"); 700 html("'/>\n");
692 html("<input type='submit' value='search'/>\n"); 701 html("<input type='submit' value='search'/>\n");
693 html("</form>"); 702 html("</form>");
694 } 703 }
695 html("</td></tr></table>\n"); 704 html("</td></tr></table>\n");
696 html("<div class='content'>"); 705 html("<div class='content'>");
697} 706}
698 707
699void cgit_print_filemode(unsigned short mode) 708void cgit_print_filemode(unsigned short mode)
700{ 709{
701 if (S_ISDIR(mode)) 710 if (S_ISDIR(mode))
702 html("d"); 711 html("d");
703 else if (S_ISLNK(mode)) 712 else if (S_ISLNK(mode))
704 html("l"); 713 html("l");
705 else if (S_ISGITLINK(mode)) 714 else if (S_ISGITLINK(mode))
706 html("m"); 715 html("m");
707 else 716 else
708 html("-"); 717 html("-");
709 html_fileperm(mode >> 6); 718 html_fileperm(mode >> 6);
710 html_fileperm(mode >> 3); 719 html_fileperm(mode >> 3);
711 html_fileperm(mode); 720 html_fileperm(mode);
712} 721}
713 722
714void cgit_print_snapshot_links(const char *repo, const char *head, 723void cgit_print_snapshot_links(const char *repo, const char *head,
715 const char *hex, int snapshots) 724 const char *hex, int snapshots)
716{ 725{
717 const struct cgit_snapshot_format* f; 726 const struct cgit_snapshot_format* f;
718 char *filename; 727 char *filename;
719 728
720 for (f = cgit_snapshot_formats; f->suffix; f++) { 729 for (f = cgit_snapshot_formats; f->suffix; f++) {
721 if (!(snapshots & f->bit)) 730 if (!(snapshots & f->bit))
722 continue; 731 continue;
723 filename = fmt("%s-%s%s", cgit_repobasename(repo), hex, 732 filename = fmt("%s-%s%s", cgit_repobasename(repo), hex,
724 f->suffix); 733 f->suffix);
725 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename); 734 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename);
726 html("<br/>"); 735 html("<br/>");
727 } 736 }
728} 737}
diff --git a/ui-shared.h b/ui-shared.h
index 2ab53ae..5a3821f 100644
--- a/ui-shared.h
+++ b/ui-shared.h
@@ -1,46 +1,49 @@
1#ifndef UI_SHARED_H 1#ifndef UI_SHARED_H
2#define UI_SHARED_H 2#define UI_SHARED_H
3 3
4extern char *cgit_hosturl(); 4extern char *cgit_hosturl();
5extern char *cgit_repourl(const char *reponame); 5extern char *cgit_repourl(const char *reponame);
6extern char *cgit_fileurl(const char *reponame, const char *pagename, 6extern char *cgit_fileurl(const char *reponame, const char *pagename,
7 const char *filename, const char *query); 7 const char *filename, const char *query);
8extern char *cgit_pageurl(const char *reponame, const char *pagename, 8extern 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-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-tree.c b/ui-tree.c
index 9876c99..4b8e7a0 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -48,128 +48,131 @@ static void print_object(const unsigned char *sha1, char *path)
48 if (buf[idx] == '\n') { 48 if (buf[idx] == '\n') {
49 buf[idx] = '\0'; 49 buf[idx] = '\0';
50 htmlf(linefmt, ++lineno); 50 htmlf(linefmt, ++lineno);
51 html_txt(buf + start); 51 html_txt(buf + start);
52 html("</td></tr>\n"); 52 html("</td></tr>\n");
53 start = idx + 1; 53 start = idx + 1;
54 } 54 }
55 idx++; 55 idx++;
56 } 56 }
57 if (start < idx) { 57 if (start < idx) {
58 htmlf(linefmt, ++lineno); 58 htmlf(linefmt, ++lineno);
59 html_txt(buf + start); 59 html_txt(buf + start);
60 } 60 }
61 html("</td></tr>\n"); 61 html("</td></tr>\n");
62 html("</table>\n"); 62 html("</table>\n");
63} 63}
64 64
65 65
66static int ls_item(const unsigned char *sha1, const char *base, int baselen, 66static int ls_item(const unsigned char *sha1, const char *base, int baselen,
67 const char *pathname, unsigned int mode, int stage, 67 const char *pathname, unsigned int mode, int stage,
68 void *cbdata) 68 void *cbdata)
69{ 69{
70 char *name; 70 char *name;
71 char *fullpath; 71 char *fullpath;
72 enum object_type type; 72 enum object_type type;
73 unsigned long size = 0; 73 unsigned long size = 0;
74 74
75 name = xstrdup(pathname); 75 name = xstrdup(pathname);
76 fullpath = fmt("%s%s%s", ctx.qry.path ? ctx.qry.path : "", 76 fullpath = fmt("%s%s%s", ctx.qry.path ? ctx.qry.path : "",
77 ctx.qry.path ? "/" : "", name); 77 ctx.qry.path ? "/" : "", name);
78 78
79 if (!S_ISGITLINK(mode)) { 79 if (!S_ISGITLINK(mode)) {
80 type = sha1_object_info(sha1, &size); 80 type = sha1_object_info(sha1, &size);
81 if (type == OBJ_BAD) { 81 if (type == OBJ_BAD) {
82 htmlf("<tr><td colspan='3'>Bad object: %s %s</td></tr>", 82 htmlf("<tr><td colspan='3'>Bad object: %s %s</td></tr>",
83 name, 83 name,
84 sha1_to_hex(sha1)); 84 sha1_to_hex(sha1));
85 return 0; 85 return 0;
86 } 86 }
87 } 87 }
88 88
89 html("<tr><td class='ls-mode'>"); 89 html("<tr><td class='ls-mode'>");
90 cgit_print_filemode(mode); 90 cgit_print_filemode(mode);
91 html("</td><td>"); 91 html("</td><td>");
92 if (S_ISGITLINK(mode)) { 92 if (S_ISGITLINK(mode)) {
93 htmlf("<a class='ls-mod' href='"); 93 htmlf("<a class='ls-mod' href='");
94 html_attr(fmt(ctx.repo->module_link, 94 html_attr(fmt(ctx.repo->module_link,
95 name, 95 name,
96 sha1_to_hex(sha1))); 96 sha1_to_hex(sha1)));
97 html("'>"); 97 html("'>");
98 html_txt(name); 98 html_txt(name);
99 html("</a>"); 99 html("</a>");
100 } else if (S_ISDIR(mode)) { 100 } else if (S_ISDIR(mode)) {
101 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, 101 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head,
102 curr_rev, fullpath); 102 curr_rev, fullpath);
103 } else { 103 } else {
104 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head, 104 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head,
105 curr_rev, fullpath); 105 curr_rev, fullpath);
106 } 106 }
107 htmlf("</td><td class='ls-size'>%li</td>", size); 107 htmlf("</td><td class='ls-size'>%li</td>", size);
108 108
109 html("<td>"); 109 html("<td>");
110 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev, 110 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev,
111 fullpath, 0, NULL, NULL, ctx.qry.showmsg); 111 fullpath, 0, NULL, NULL, ctx.qry.showmsg);
112 if (ctx.repo->max_stats)
113 cgit_stats_link("stats", NULL, "button", ctx.qry.head,
114 fullpath);
112 html("</td></tr>\n"); 115 html("</td></tr>\n");
113 free(name); 116 free(name);
114 return 0; 117 return 0;
115} 118}
116 119
117static void ls_head() 120static void ls_head()
118{ 121{
119 html("<table summary='tree listing' class='list'>\n"); 122 html("<table summary='tree listing' class='list'>\n");
120 html("<tr class='nohover'>"); 123 html("<tr class='nohover'>");
121 html("<th class='left'>Mode</th>"); 124 html("<th class='left'>Mode</th>");
122 html("<th class='left'>Name</th>"); 125 html("<th class='left'>Name</th>");
123 html("<th class='right'>Size</th>"); 126 html("<th class='right'>Size</th>");
124 html("<th/>"); 127 html("<th/>");
125 html("</tr>\n"); 128 html("</tr>\n");
126 header = 1; 129 header = 1;
127} 130}
128 131
129static void ls_tail() 132static void ls_tail()
130{ 133{
131 if (!header) 134 if (!header)
132 return; 135 return;
133 html("</table>\n"); 136 html("</table>\n");
134 header = 0; 137 header = 0;
135} 138}
136 139
137static void ls_tree(const unsigned char *sha1, char *path) 140static void ls_tree(const unsigned char *sha1, char *path)
138{ 141{
139 struct tree *tree; 142 struct tree *tree;
140 143
141 tree = parse_tree_indirect(sha1); 144 tree = parse_tree_indirect(sha1);
142 if (!tree) { 145 if (!tree) {
143 cgit_print_error(fmt("Not a tree object: %s", 146 cgit_print_error(fmt("Not a tree object: %s",
144 sha1_to_hex(sha1))); 147 sha1_to_hex(sha1)));
145 return; 148 return;
146 } 149 }
147 150
148 ls_head(); 151 ls_head();
149 read_tree_recursive(tree, "", 0, 1, NULL, ls_item, NULL); 152 read_tree_recursive(tree, "", 0, 1, NULL, ls_item, NULL);
150 ls_tail(); 153 ls_tail();
151} 154}
152 155
153 156
154static int walk_tree(const unsigned char *sha1, const char *base, int baselen, 157static int walk_tree(const unsigned char *sha1, const char *base, int baselen,
155 const char *pathname, unsigned mode, int stage, 158 const char *pathname, unsigned mode, int stage,
156 void *cbdata) 159 void *cbdata)
157{ 160{
158 static int state; 161 static int state;
159 static char buffer[PATH_MAX]; 162 static char buffer[PATH_MAX];
160 char *url; 163 char *url;
161 164
162 if (state == 0) { 165 if (state == 0) {
163 memcpy(buffer, base, baselen); 166 memcpy(buffer, base, baselen);
164 strcpy(buffer+baselen, pathname); 167 strcpy(buffer+baselen, pathname);
165 url = cgit_pageurl(ctx.qry.repo, "tree", 168 url = cgit_pageurl(ctx.qry.repo, "tree",
166 fmt("h=%s&amp;path=%s", curr_rev, buffer)); 169 fmt("h=%s&amp;path=%s", curr_rev, buffer));
167 html("/"); 170 html("/");
168 cgit_tree_link(xstrdup(pathname), NULL, NULL, ctx.qry.head, 171 cgit_tree_link(xstrdup(pathname), NULL, NULL, ctx.qry.head,
169 curr_rev, buffer); 172 curr_rev, buffer);
170 173
171 if (strcmp(match_path, buffer)) 174 if (strcmp(match_path, buffer))
172 return READ_TREE_RECURSIVE; 175 return READ_TREE_RECURSIVE;
173 176
174 if (S_ISDIR(mode)) { 177 if (S_ISDIR(mode)) {
175 state = 1; 178 state = 1;