summaryrefslogtreecommitdiffabout
authorLars Hjemli <hjemli@gmail.com>2008-12-07 12:17:21 (UTC)
committer Lars Hjemli <hjemli@gmail.com>2008-12-07 12:17:21 (UTC)
commitfb2f3f6c29bad733723152893c5246a756e4cada (patch) (unidiff)
tree5b2953c0c116f276ca48beee676a6662b6329d95
parentc6a6aa2186daf39814baa0e71378c2e9e1041002 (diff)
downloadcgit-fb2f3f6c29bad733723152893c5246a756e4cada.zip
cgit-fb2f3f6c29bad733723152893c5246a756e4cada.tar.gz
cgit-fb2f3f6c29bad733723152893c5246a756e4cada.tar.bz2
ui-stats: replace 'enable-stats' setting with 'max-stats'
The new 'max-stats' and 'repo.max-stats' settings makes it possible to define the maximum statistics period, both globally and per repo. Hence, it is now feasible to allow statistics on repositories with a high commit frequency, like linux-2.6, by setting repo.max-stats to e.g. 'month'. Signed-off-by: Lars Hjemli <hjemli@gmail.com>
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--cgit.c10
-rw-r--r--cgit.h4
-rw-r--r--cgitrc.5.txt23
-rw-r--r--cmd.c5
-rw-r--r--shared.c2
-rw-r--r--ui-shared.c2
-rw-r--r--ui-stats.c97
-rw-r--r--ui-stats.h19
8 files changed, 104 insertions, 58 deletions
diff --git a/cgit.c b/cgit.c
index 22b6d7c..57e11cd 100644
--- a/cgit.c
+++ b/cgit.c
@@ -1,233 +1,235 @@
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);
57 else if (!strcmp(name, "enable-stats")) 58 else if (!strcmp(name, "max-stats"))
58 ctx.cfg.enable_stats = atoi(value); 59 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
59 else if (!strcmp(name, "cache-size")) 60 else if (!strcmp(name, "cache-size"))
60 ctx.cfg.cache_size = atoi(value); 61 ctx.cfg.cache_size = atoi(value);
61 else if (!strcmp(name, "cache-root")) 62 else if (!strcmp(name, "cache-root"))
62 ctx.cfg.cache_root = xstrdup(value); 63 ctx.cfg.cache_root = xstrdup(value);
63 else if (!strcmp(name, "cache-root-ttl")) 64 else if (!strcmp(name, "cache-root-ttl"))
64 ctx.cfg.cache_root_ttl = atoi(value); 65 ctx.cfg.cache_root_ttl = atoi(value);
65 else if (!strcmp(name, "cache-repo-ttl")) 66 else if (!strcmp(name, "cache-repo-ttl"))
66 ctx.cfg.cache_repo_ttl = atoi(value); 67 ctx.cfg.cache_repo_ttl = atoi(value);
67 else if (!strcmp(name, "cache-static-ttl")) 68 else if (!strcmp(name, "cache-static-ttl"))
68 ctx.cfg.cache_static_ttl = atoi(value); 69 ctx.cfg.cache_static_ttl = atoi(value);
69 else if (!strcmp(name, "cache-dynamic-ttl")) 70 else if (!strcmp(name, "cache-dynamic-ttl"))
70 ctx.cfg.cache_dynamic_ttl = atoi(value); 71 ctx.cfg.cache_dynamic_ttl = atoi(value);
71 else if (!strcmp(name, "max-message-length")) 72 else if (!strcmp(name, "max-message-length"))
72 ctx.cfg.max_msg_len = atoi(value); 73 ctx.cfg.max_msg_len = atoi(value);
73 else if (!strcmp(name, "max-repodesc-length")) 74 else if (!strcmp(name, "max-repodesc-length"))
74 ctx.cfg.max_repodesc_len = atoi(value); 75 ctx.cfg.max_repodesc_len = atoi(value);
75 else if (!strcmp(name, "max-repo-count")) 76 else if (!strcmp(name, "max-repo-count"))
76 ctx.cfg.max_repo_count = atoi(value); 77 ctx.cfg.max_repo_count = atoi(value);
77 else if (!strcmp(name, "max-commit-count")) 78 else if (!strcmp(name, "max-commit-count"))
78 ctx.cfg.max_commit_count = atoi(value); 79 ctx.cfg.max_commit_count = atoi(value);
79 else if (!strcmp(name, "summary-log")) 80 else if (!strcmp(name, "summary-log"))
80 ctx.cfg.summary_log = atoi(value); 81 ctx.cfg.summary_log = atoi(value);
81 else if (!strcmp(name, "summary-branches")) 82 else if (!strcmp(name, "summary-branches"))
82 ctx.cfg.summary_branches = atoi(value); 83 ctx.cfg.summary_branches = atoi(value);
83 else if (!strcmp(name, "summary-tags")) 84 else if (!strcmp(name, "summary-tags"))
84 ctx.cfg.summary_tags = atoi(value); 85 ctx.cfg.summary_tags = atoi(value);
85 else if (!strcmp(name, "agefile")) 86 else if (!strcmp(name, "agefile"))
86 ctx.cfg.agefile = xstrdup(value); 87 ctx.cfg.agefile = xstrdup(value);
87 else if (!strcmp(name, "renamelimit")) 88 else if (!strcmp(name, "renamelimit"))
88 ctx.cfg.renamelimit = atoi(value); 89 ctx.cfg.renamelimit = atoi(value);
89 else if (!strcmp(name, "robots")) 90 else if (!strcmp(name, "robots"))
90 ctx.cfg.robots = xstrdup(value); 91 ctx.cfg.robots = xstrdup(value);
91 else if (!strcmp(name, "clone-prefix")) 92 else if (!strcmp(name, "clone-prefix"))
92 ctx.cfg.clone_prefix = xstrdup(value); 93 ctx.cfg.clone_prefix = xstrdup(value);
93 else if (!strcmp(name, "local-time")) 94 else if (!strcmp(name, "local-time"))
94 ctx.cfg.local_time = atoi(value); 95 ctx.cfg.local_time = atoi(value);
95 else if (!strcmp(name, "repo.group")) 96 else if (!strcmp(name, "repo.group"))
96 ctx.cfg.repo_group = xstrdup(value); 97 ctx.cfg.repo_group = xstrdup(value);
97 else if (!strcmp(name, "repo.url")) 98 else if (!strcmp(name, "repo.url"))
98 ctx.repo = cgit_add_repo(value); 99 ctx.repo = cgit_add_repo(value);
99 else if (!strcmp(name, "repo.name")) 100 else if (!strcmp(name, "repo.name"))
100 ctx.repo->name = xstrdup(value); 101 ctx.repo->name = xstrdup(value);
101 else if (ctx.repo && !strcmp(name, "repo.path")) 102 else if (ctx.repo && !strcmp(name, "repo.path"))
102 ctx.repo->path = trim_end(value, '/'); 103 ctx.repo->path = trim_end(value, '/');
103 else if (ctx.repo && !strcmp(name, "repo.clone-url")) 104 else if (ctx.repo && !strcmp(name, "repo.clone-url"))
104 ctx.repo->clone_url = xstrdup(value); 105 ctx.repo->clone_url = xstrdup(value);
105 else if (ctx.repo && !strcmp(name, "repo.desc")) 106 else if (ctx.repo && !strcmp(name, "repo.desc"))
106 ctx.repo->desc = xstrdup(value); 107 ctx.repo->desc = xstrdup(value);
107 else if (ctx.repo && !strcmp(name, "repo.owner")) 108 else if (ctx.repo && !strcmp(name, "repo.owner"))
108 ctx.repo->owner = xstrdup(value); 109 ctx.repo->owner = xstrdup(value);
109 else if (ctx.repo && !strcmp(name, "repo.defbranch")) 110 else if (ctx.repo && !strcmp(name, "repo.defbranch"))
110 ctx.repo->defbranch = xstrdup(value); 111 ctx.repo->defbranch = xstrdup(value);
111 else if (ctx.repo && !strcmp(name, "repo.snapshots")) 112 else if (ctx.repo && !strcmp(name, "repo.snapshots"))
112 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: &? */
113 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount")) 114 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount"))
114 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);
115 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount")) 116 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
116 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);
117 else if (ctx.repo && !strcmp(name, "repo.enable-stats")) 118 else if (ctx.repo && !strcmp(name, "repo.max-stats"))
118 ctx.repo->enable_stats = ctx.cfg.enable_stats && atoi(value); 119 ctx.repo->max_stats = cgit_find_stats_period(value, NULL);
119 else if (ctx.repo && !strcmp(name, "repo.module-link")) 120 else if (ctx.repo && !strcmp(name, "repo.module-link"))
120 ctx.repo->module_link= xstrdup(value); 121 ctx.repo->module_link= xstrdup(value);
121 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) { 122 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
122 if (*value == '/') 123 if (*value == '/')
123 ctx.repo->readme = xstrdup(value); 124 ctx.repo->readme = xstrdup(value);
124 else 125 else
125 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value)); 126 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
126 } else if (!strcmp(name, "include")) 127 } else if (!strcmp(name, "include"))
127 parse_configfile(value, config_cb); 128 parse_configfile(value, config_cb);
128} 129}
129 130
130static void querystring_cb(const char *name, const char *value) 131static void querystring_cb(const char *name, const char *value)
131{ 132{
132 if (!strcmp(name,"r")) { 133 if (!strcmp(name,"r")) {
133 ctx.qry.repo = xstrdup(value); 134 ctx.qry.repo = xstrdup(value);
134 ctx.repo = cgit_get_repoinfo(value); 135 ctx.repo = cgit_get_repoinfo(value);
135 } else if (!strcmp(name, "p")) { 136 } else if (!strcmp(name, "p")) {
136 ctx.qry.page = xstrdup(value); 137 ctx.qry.page = xstrdup(value);
137 } else if (!strcmp(name, "url")) { 138 } else if (!strcmp(name, "url")) {
138 ctx.qry.url = xstrdup(value); 139 ctx.qry.url = xstrdup(value);
139 cgit_parse_url(value); 140 cgit_parse_url(value);
140 } else if (!strcmp(name, "qt")) { 141 } else if (!strcmp(name, "qt")) {
141 ctx.qry.grep = xstrdup(value); 142 ctx.qry.grep = xstrdup(value);
142 } else if (!strcmp(name, "q")) { 143 } else if (!strcmp(name, "q")) {
143 ctx.qry.search = xstrdup(value); 144 ctx.qry.search = xstrdup(value);
144 } else if (!strcmp(name, "h")) { 145 } else if (!strcmp(name, "h")) {
145 ctx.qry.head = xstrdup(value); 146 ctx.qry.head = xstrdup(value);
146 ctx.qry.has_symref = 1; 147 ctx.qry.has_symref = 1;
147 } else if (!strcmp(name, "id")) { 148 } else if (!strcmp(name, "id")) {
148 ctx.qry.sha1 = xstrdup(value); 149 ctx.qry.sha1 = xstrdup(value);
149 ctx.qry.has_sha1 = 1; 150 ctx.qry.has_sha1 = 1;
150 } else if (!strcmp(name, "id2")) { 151 } else if (!strcmp(name, "id2")) {
151 ctx.qry.sha2 = xstrdup(value); 152 ctx.qry.sha2 = xstrdup(value);
152 ctx.qry.has_sha1 = 1; 153 ctx.qry.has_sha1 = 1;
153 } else if (!strcmp(name, "ofs")) { 154 } else if (!strcmp(name, "ofs")) {
154 ctx.qry.ofs = atoi(value); 155 ctx.qry.ofs = atoi(value);
155 } else if (!strcmp(name, "path")) { 156 } else if (!strcmp(name, "path")) {
156 ctx.qry.path = trim_end(value, '/'); 157 ctx.qry.path = trim_end(value, '/');
157 } else if (!strcmp(name, "name")) { 158 } else if (!strcmp(name, "name")) {
158 ctx.qry.name = xstrdup(value); 159 ctx.qry.name = xstrdup(value);
159 } else if (!strcmp(name, "mimetype")) { 160 } else if (!strcmp(name, "mimetype")) {
160 ctx.qry.mimetype = xstrdup(value); 161 ctx.qry.mimetype = xstrdup(value);
161 } else if (!strcmp(name, "period")) { 162 } else if (!strcmp(name, "period")) {
162 ctx.qry.period = xstrdup(value); 163 ctx.qry.period = xstrdup(value);
163 } 164 }
164} 165}
165 166
166static void prepare_context(struct cgit_context *ctx) 167static void prepare_context(struct cgit_context *ctx)
167{ 168{
168 memset(ctx, 0, sizeof(ctx)); 169 memset(ctx, 0, sizeof(ctx));
169 ctx->cfg.agefile = "info/web/last-modified"; 170 ctx->cfg.agefile = "info/web/last-modified";
170 ctx->cfg.nocache = 0; 171 ctx->cfg.nocache = 0;
171 ctx->cfg.cache_size = 0; 172 ctx->cfg.cache_size = 0;
172 ctx->cfg.cache_dynamic_ttl = 5; 173 ctx->cfg.cache_dynamic_ttl = 5;
173 ctx->cfg.cache_max_create_time = 5; 174 ctx->cfg.cache_max_create_time = 5;
174 ctx->cfg.cache_repo_ttl = 5; 175 ctx->cfg.cache_repo_ttl = 5;
175 ctx->cfg.cache_root = CGIT_CACHE_ROOT; 176 ctx->cfg.cache_root = CGIT_CACHE_ROOT;
176 ctx->cfg.cache_root_ttl = 5; 177 ctx->cfg.cache_root_ttl = 5;
177 ctx->cfg.cache_static_ttl = -1; 178 ctx->cfg.cache_static_ttl = -1;
178 ctx->cfg.css = "/cgit.css"; 179 ctx->cfg.css = "/cgit.css";
179 ctx->cfg.logo = "/git-logo.png"; 180 ctx->cfg.logo = "/git-logo.png";
180 ctx->cfg.local_time = 0; 181 ctx->cfg.local_time = 0;
181 ctx->cfg.max_repo_count = 50; 182 ctx->cfg.max_repo_count = 50;
182 ctx->cfg.max_commit_count = 50; 183 ctx->cfg.max_commit_count = 50;
183 ctx->cfg.max_lock_attempts = 5; 184 ctx->cfg.max_lock_attempts = 5;
184 ctx->cfg.max_msg_len = 80; 185 ctx->cfg.max_msg_len = 80;
185 ctx->cfg.max_repodesc_len = 80; 186 ctx->cfg.max_repodesc_len = 80;
187 ctx->cfg.max_stats = 0;
186 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; 188 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s";
187 ctx->cfg.renamelimit = -1; 189 ctx->cfg.renamelimit = -1;
188 ctx->cfg.robots = "index, nofollow"; 190 ctx->cfg.robots = "index, nofollow";
189 ctx->cfg.root_title = "Git repository browser"; 191 ctx->cfg.root_title = "Git repository browser";
190 ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 192 ctx->cfg.root_desc = "a fast webinterface for the git dscm";
191 ctx->cfg.script_name = CGIT_SCRIPT_NAME; 193 ctx->cfg.script_name = CGIT_SCRIPT_NAME;
192 ctx->cfg.summary_branches = 10; 194 ctx->cfg.summary_branches = 10;
193 ctx->cfg.summary_log = 10; 195 ctx->cfg.summary_log = 10;
194 ctx->cfg.summary_tags = 10; 196 ctx->cfg.summary_tags = 10;
195 ctx->page.mimetype = "text/html"; 197 ctx->page.mimetype = "text/html";
196 ctx->page.charset = PAGE_ENCODING; 198 ctx->page.charset = PAGE_ENCODING;
197 ctx->page.filename = NULL; 199 ctx->page.filename = NULL;
198 ctx->page.size = 0; 200 ctx->page.size = 0;
199 ctx->page.modified = time(NULL); 201 ctx->page.modified = time(NULL);
200 ctx->page.expires = ctx->page.modified; 202 ctx->page.expires = ctx->page.modified;
201} 203}
202 204
203struct refmatch { 205struct refmatch {
204 char *req_ref; 206 char *req_ref;
205 char *first_ref; 207 char *first_ref;
206 int match; 208 int match;
207}; 209};
208 210
209int find_current_ref(const char *refname, const unsigned char *sha1, 211int find_current_ref(const char *refname, const unsigned char *sha1,
210 int flags, void *cb_data) 212 int flags, void *cb_data)
211{ 213{
212 struct refmatch *info; 214 struct refmatch *info;
213 215
214 info = (struct refmatch *)cb_data; 216 info = (struct refmatch *)cb_data;
215 if (!strcmp(refname, info->req_ref)) 217 if (!strcmp(refname, info->req_ref))
216 info->match = 1; 218 info->match = 1;
217 if (!info->first_ref) 219 if (!info->first_ref)
218 info->first_ref = xstrdup(refname); 220 info->first_ref = xstrdup(refname);
219 return info->match; 221 return info->match;
220} 222}
221 223
222char *find_default_branch(struct cgit_repo *repo) 224char *find_default_branch(struct cgit_repo *repo)
223{ 225{
224 struct refmatch info; 226 struct refmatch info;
225 char *ref; 227 char *ref;
226 228
227 info.req_ref = repo->defbranch; 229 info.req_ref = repo->defbranch;
228 info.first_ref = NULL; 230 info.first_ref = NULL;
229 info.match = 0; 231 info.match = 0;
230 for_each_branch_ref(find_current_ref, &info); 232 for_each_branch_ref(find_current_ref, &info);
231 if (info.match) 233 if (info.match)
232 ref = info.req_ref; 234 ref = info.req_ref;
233 else 235 else
diff --git a/cgit.h b/cgit.h
index 85045c4..f2cb671 100644
--- a/cgit.h
+++ b/cgit.h
@@ -16,195 +16,195 @@
16#include <log-tree.h> 16#include <log-tree.h>
17#include <archive.h> 17#include <archive.h>
18#include <xdiff/xdiff.h> 18#include <xdiff/xdiff.h>
19#include <utf8.h> 19#include <utf8.h>
20 20
21 21
22/* 22/*
23 * Dateformats used on misc. pages 23 * Dateformats used on misc. pages
24 */ 24 */
25#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)" 25#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)"
26#define FMT_SHORTDATE "%Y-%m-%d" 26#define FMT_SHORTDATE "%Y-%m-%d"
27#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ" 27#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ"
28 28
29 29
30/* 30/*
31 * Limits used for relative dates 31 * Limits used for relative dates
32 */ 32 */
33#define TM_MIN 60 33#define TM_MIN 60
34#define TM_HOUR (TM_MIN * 60) 34#define TM_HOUR (TM_MIN * 60)
35#define TM_DAY (TM_HOUR * 24) 35#define TM_DAY (TM_HOUR * 24)
36#define TM_WEEK (TM_DAY * 7) 36#define TM_WEEK (TM_DAY * 7)
37#define TM_YEAR (TM_DAY * 365) 37#define TM_YEAR (TM_DAY * 365)
38#define TM_MONTH (TM_YEAR / 12.0) 38#define TM_MONTH (TM_YEAR / 12.0)
39 39
40 40
41/* 41/*
42 * Default encoding 42 * Default encoding
43 */ 43 */
44#define PAGE_ENCODING "UTF-8" 44#define PAGE_ENCODING "UTF-8"
45 45
46typedef void (*configfn)(const char *name, const char *value); 46typedef void (*configfn)(const char *name, const char *value);
47typedef void (*filepair_fn)(struct diff_filepair *pair); 47typedef void (*filepair_fn)(struct diff_filepair *pair);
48typedef void (*linediff_fn)(char *line, int len); 48typedef void (*linediff_fn)(char *line, int len);
49 49
50struct cgit_repo { 50struct cgit_repo {
51 char *url; 51 char *url;
52 char *name; 52 char *name;
53 char *path; 53 char *path;
54 char *desc; 54 char *desc;
55 char *owner; 55 char *owner;
56 char *defbranch; 56 char *defbranch;
57 char *group; 57 char *group;
58 char *module_link; 58 char *module_link;
59 char *readme; 59 char *readme;
60 char *clone_url; 60 char *clone_url;
61 int snapshots; 61 int snapshots;
62 int enable_log_filecount; 62 int enable_log_filecount;
63 int enable_log_linecount; 63 int enable_log_linecount;
64 int enable_stats; 64 int max_stats;
65}; 65};
66 66
67struct cgit_repolist { 67struct cgit_repolist {
68 int length; 68 int length;
69 int count; 69 int count;
70 struct cgit_repo *repos; 70 struct cgit_repo *repos;
71}; 71};
72 72
73struct commitinfo { 73struct commitinfo {
74 struct commit *commit; 74 struct commit *commit;
75 char *author; 75 char *author;
76 char *author_email; 76 char *author_email;
77 unsigned long author_date; 77 unsigned long author_date;
78 char *committer; 78 char *committer;
79 char *committer_email; 79 char *committer_email;
80 unsigned long committer_date; 80 unsigned long committer_date;
81 char *subject; 81 char *subject;
82 char *msg; 82 char *msg;
83 char *msg_encoding; 83 char *msg_encoding;
84}; 84};
85 85
86struct taginfo { 86struct taginfo {
87 char *tagger; 87 char *tagger;
88 char *tagger_email; 88 char *tagger_email;
89 unsigned long tagger_date; 89 unsigned long tagger_date;
90 char *msg; 90 char *msg;
91}; 91};
92 92
93struct refinfo { 93struct refinfo {
94 const char *refname; 94 const char *refname;
95 struct object *object; 95 struct object *object;
96 union { 96 union {
97 struct taginfo *tag; 97 struct taginfo *tag;
98 struct commitinfo *commit; 98 struct commitinfo *commit;
99 }; 99 };
100}; 100};
101 101
102struct reflist { 102struct reflist {
103 struct refinfo **refs; 103 struct refinfo **refs;
104 int alloc; 104 int alloc;
105 int count; 105 int count;
106}; 106};
107 107
108struct cgit_query { 108struct cgit_query {
109 int has_symref; 109 int has_symref;
110 int has_sha1; 110 int has_sha1;
111 char *raw; 111 char *raw;
112 char *repo; 112 char *repo;
113 char *page; 113 char *page;
114 char *search; 114 char *search;
115 char *grep; 115 char *grep;
116 char *head; 116 char *head;
117 char *sha1; 117 char *sha1;
118 char *sha2; 118 char *sha2;
119 char *path; 119 char *path;
120 char *name; 120 char *name;
121 char *mimetype; 121 char *mimetype;
122 char *url; 122 char *url;
123 char *period; 123 char *period;
124 int ofs; 124 int ofs;
125 int nohead; 125 int nohead;
126}; 126};
127 127
128struct cgit_config { 128struct cgit_config {
129 char *agefile; 129 char *agefile;
130 char *cache_root; 130 char *cache_root;
131 char *clone_prefix; 131 char *clone_prefix;
132 char *css; 132 char *css;
133 char *favicon; 133 char *favicon;
134 char *footer; 134 char *footer;
135 char *index_header; 135 char *index_header;
136 char *index_info; 136 char *index_info;
137 char *logo; 137 char *logo;
138 char *logo_link; 138 char *logo_link;
139 char *module_link; 139 char *module_link;
140 char *repo_group; 140 char *repo_group;
141 char *robots; 141 char *robots;
142 char *root_title; 142 char *root_title;
143 char *root_desc; 143 char *root_desc;
144 char *root_readme; 144 char *root_readme;
145 char *script_name; 145 char *script_name;
146 char *virtual_root; 146 char *virtual_root;
147 int cache_size; 147 int cache_size;
148 int cache_dynamic_ttl; 148 int cache_dynamic_ttl;
149 int cache_max_create_time; 149 int cache_max_create_time;
150 int cache_repo_ttl; 150 int cache_repo_ttl;
151 int cache_root_ttl; 151 int cache_root_ttl;
152 int cache_static_ttl; 152 int cache_static_ttl;
153 int enable_index_links; 153 int enable_index_links;
154 int enable_log_filecount; 154 int enable_log_filecount;
155 int enable_log_linecount; 155 int enable_log_linecount;
156 int enable_stats;
157 int local_time; 156 int local_time;
158 int max_repo_count; 157 int max_repo_count;
159 int max_commit_count; 158 int max_commit_count;
160 int max_lock_attempts; 159 int max_lock_attempts;
161 int max_msg_len; 160 int max_msg_len;
162 int max_repodesc_len; 161 int max_repodesc_len;
162 int max_stats;
163 int nocache; 163 int nocache;
164 int renamelimit; 164 int renamelimit;
165 int snapshots; 165 int snapshots;
166 int summary_branches; 166 int summary_branches;
167 int summary_log; 167 int summary_log;
168 int summary_tags; 168 int summary_tags;
169}; 169};
170 170
171struct cgit_page { 171struct cgit_page {
172 time_t modified; 172 time_t modified;
173 time_t expires; 173 time_t expires;
174 size_t size; 174 size_t size;
175 char *mimetype; 175 char *mimetype;
176 char *charset; 176 char *charset;
177 char *filename; 177 char *filename;
178 char *title; 178 char *title;
179}; 179};
180 180
181struct cgit_context { 181struct cgit_context {
182 struct cgit_query qry; 182 struct cgit_query qry;
183 struct cgit_config cfg; 183 struct cgit_config cfg;
184 struct cgit_repo *repo; 184 struct cgit_repo *repo;
185 struct cgit_page page; 185 struct cgit_page page;
186}; 186};
187 187
188struct cgit_snapshot_format { 188struct cgit_snapshot_format {
189 const char *suffix; 189 const char *suffix;
190 const char *mimetype; 190 const char *mimetype;
191 write_archive_fn_t write_func; 191 write_archive_fn_t write_func;
192 int bit; 192 int bit;
193}; 193};
194 194
195extern const char *cgit_version; 195extern const char *cgit_version;
196 196
197extern struct cgit_repolist cgit_repolist; 197extern struct cgit_repolist cgit_repolist;
198extern struct cgit_context ctx; 198extern struct cgit_context ctx;
199extern const struct cgit_snapshot_format cgit_snapshot_formats[]; 199extern const struct cgit_snapshot_format cgit_snapshot_formats[];
200 200
201extern struct cgit_repo *cgit_add_repo(const char *url); 201extern struct cgit_repo *cgit_add_repo(const char *url);
202extern struct cgit_repo *cgit_get_repoinfo(const char *url); 202extern struct cgit_repo *cgit_get_repoinfo(const char *url);
203extern void cgit_repo_config_cb(const char *name, const char *value); 203extern void cgit_repo_config_cb(const char *name, const char *value);
204 204
205extern int chk_zero(int result, char *msg); 205extern int chk_zero(int result, char *msg);
206extern int chk_positive(int result, char *msg); 206extern int chk_positive(int result, char *msg);
207extern int chk_non_negative(int result, char *msg); 207extern int chk_non_negative(int result, char *msg);
208 208
209extern char *trim_end(const char *str, char c); 209extern char *trim_end(const char *str, char c);
210extern char *strlpart(char *txt, int maxlen); 210extern char *strlpart(char *txt, int maxlen);
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 60d3ea4..0bbbea3 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -29,347 +29,356 @@ cache-root
29 29
30cache-dynamic-ttl 30cache-dynamic-ttl
31 Number which specifies the time-to-live, in minutes, for the cached 31 Number which specifies the time-to-live, in minutes, for the cached
32 version of repository pages accessed without a fixed SHA1. Default 32 version of repository pages accessed without a fixed SHA1. Default
33 value: "5". 33 value: "5".
34 34
35cache-repo-ttl 35cache-repo-ttl
36 Number which specifies the time-to-live, in minutes, for the cached 36 Number which specifies the time-to-live, in minutes, for the cached
37 version of the repository summary page. Default value: "5". 37 version of the repository summary page. Default value: "5".
38 38
39cache-root-ttl 39cache-root-ttl
40 Number which specifies the time-to-live, in minutes, for the cached 40 Number which specifies the time-to-live, in minutes, for the cached
41 version of the repository index page. Default value: "5". 41 version of the repository index page. Default value: "5".
42 42
43cache-size 43cache-size
44 The maximum number of entries in the cgit cache. Default value: "0" 44 The maximum number of entries in the cgit cache. Default value: "0"
45 (i.e. caching is disabled). 45 (i.e. caching is disabled).
46 46
47cache-static-ttl 47cache-static-ttl
48 Number which specifies the time-to-live, in minutes, for the cached 48 Number which specifies the time-to-live, in minutes, for the cached
49 version of repository pages accessed with a fixed SHA1. Default value: 49 version of repository pages accessed with a fixed SHA1. Default value:
50 "5". 50 "5".
51 51
52clone-prefix 52clone-prefix
53 Space-separated list of common prefixes which, when combined with a 53 Space-separated list of common prefixes which, when combined with a
54 repository url, generates valid clone urls for the repository. This 54 repository url, generates valid clone urls for the repository. This
55 setting is only used if `repo.clone-url` is unspecified. Default value: 55 setting is only used if `repo.clone-url` is unspecified. Default value:
56 none. 56 none.
57 57
58css 58css
59 Url which specifies the css document to include in all cgit pages. 59 Url which specifies the css document to include in all cgit pages.
60 Default value: "/cgit.css". 60 Default value: "/cgit.css".
61 61
62enable-index-links 62enable-index-links
63 Flag which, when set to "1", will make cgit generate extra links for 63 Flag which, when set to "1", will make cgit generate extra links for
64 each repo in the repository index (specifically, to the "summary", 64 each repo in the repository index (specifically, to the "summary",
65 "commit" and "tree" pages). Default value: "0". 65 "commit" and "tree" pages). Default value: "0".
66 66
67enable-log-filecount 67enable-log-filecount
68 Flag which, when set to "1", will make cgit print the number of 68 Flag which, when set to "1", will make cgit print the number of
69 modified files for each commit on the repository log page. Default 69 modified files for each commit on the repository log page. Default
70 value: "0". 70 value: "0".
71 71
72enable-log-linecount 72enable-log-linecount
73 Flag which, when set to "1", will make cgit print the number of added 73 Flag which, when set to "1", will make cgit print the number of added
74 and removed lines for each commit on the repository log page. Default 74 and removed lines for each commit on the repository log page. Default
75 value: "0". 75 value: "0".
76 76
77enable-stats
78 Globally enable/disable statistics for each repository. Default
79 value: "0".
80
81favicon 77favicon
82 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
83 suggested to use the value "/favicon.ico" since certain browsers will 79 suggested to use the value "/favicon.ico" since certain browsers will
84 ignore other values. Default value: none. 80 ignore other values. Default value: none.
85 81
86footer 82footer
87 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
88 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
89 "generated by..." message. Default value: none. 85 "generated by..." message. Default value: none.
90 86
91include 87include
92 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-
93 file is parsed. Default value: none. 89 file is parsed. Default value: none.
94 90
95index-header 91index-header
96 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
97 verbatim above the repository index. This setting is deprecated, and 93 verbatim above the repository index. This setting is deprecated, and
98 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
99 value: none. 95 value: none.
100 96
101index-info 97index-info
102 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
103 verbatim below the heading on the repository index page. This setting 99 verbatim below the heading on the repository index page. This setting
104 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
105 instead). Default value: none. 101 instead). Default value: none.
106 102
107local-time 103local-time
108 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
109 servers timezone. Default value: "0". 105 servers timezone. Default value: "0".
110 106
111logo 107logo
112 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
113 on all cgit pages. 109 on all cgit pages.
114 110
115logo-link 111logo-link
116 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
117 calculated url of the repository index page will be used. Default 113 calculated url of the repository index page will be used. Default
118 value: none. 114 value: none.
119 115
120max-commit-count 116max-commit-count
121 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
122 value: "50". 118 value: "50".
123 119
124max-message-length 120max-message-length
125 Specifies the maximum number of commit message characters to display in 121 Specifies the maximum number of commit message characters to display in
126 "log" view. Default value: "80". 122 "log" view. Default value: "80".
127 123
128max-repo-count 124max-repo-count
129 Specifies the number of entries to list per page on therepository 125 Specifies the number of entries to list per page on therepository
130 index page. Default value: "50". 126 index page. Default value: "50".
131 127
132max-repodesc-length 128max-repodesc-length
133 Specifies the maximum number of repo description characters to display 129 Specifies the maximum number of repo description characters to display
134 on the repository index page. Default value: "80". 130 on the repository index page. Default value: "80".
135 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
136module-link 137module-link
137 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
138 submodule is printed in a directory listing. The arguments for the 139 submodule is printed in a directory listing. The arguments for the
139 formatstring are the path and SHA1 of the submodule commit. Default 140 formatstring are the path and SHA1 of the submodule commit. Default
140 value: "./?repo=%s&page=commit&id=%s" 141 value: "./?repo=%s&page=commit&id=%s"
141 142
142nocache 143nocache
143 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
144 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
145 value: "0". 146 value: "0".
146 147
147renamelimit 148renamelimit
148 Maximum number of files to consider when detecting renames. The value 149 Maximum number of files to consider when detecting renames. The value
149 "-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
150 `man git-diff`). Default value: "-1". 151 `man git-diff`). Default value: "-1".
151 152
152repo.group 153repo.group
153 A value for the current repository group, which all repositories 154 A value for the current repository group, which all repositories
154 specified after this setting will inherit. Default value: none. 155 specified after this setting will inherit. Default value: none.
155 156
156robots 157robots
157 Text used as content for the "robots" meta-tag. Default value: 158 Text used as content for the "robots" meta-tag. Default value:
158 "index, nofollow". 159 "index, nofollow".
159 160
160root-desc 161root-desc
161 Text printed below the heading on the repository index page. Default 162 Text printed below the heading on the repository index page. Default
162 value: "a fast webinterface for the git dscm". 163 value: "a fast webinterface for the git dscm".
163 164
164root-readme: 165root-readme:
165 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
166 verbatim below the "about" link on the repository index page. Default 167 verbatim below the "about" link on the repository index page. Default
167 value: none. 168 value: none.
168 169
169root-title 170root-title
170 Text printed as heading on the repository index page. Default value: 171 Text printed as heading on the repository index page. Default value:
171 "Git Repository Browser". 172 "Git Repository Browser".
172 173
173snapshots 174snapshots
174 Text which specifies the default (and allowed) set of snapshot formats 175 Text which specifies the default (and allowed) set of snapshot formats
175 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
176 of the following values: 177 of the following values:
177 "tar" uncompressed tar-file 178 "tar" uncompressed tar-file
178 "tar.gz"gzip-compressed tar-file 179 "tar.gz"gzip-compressed tar-file
179 "tar.bz2"bzip-compressed tar-file 180 "tar.bz2"bzip-compressed tar-file
180 "zip" zip-file 181 "zip" zip-file
181 Default value: none. 182 Default value: none.
182 183
183summary-branches 184summary-branches
184 Specifies the number of branches to display in the repository "summary" 185 Specifies the number of branches to display in the repository "summary"
185 view. Default value: "10". 186 view. Default value: "10".
186 187
187summary-log 188summary-log
188 Specifies the number of log entries to display in the repository 189 Specifies the number of log entries to display in the repository
189 "summary" view. Default value: "10". 190 "summary" view. Default value: "10".
190 191
191summary-tags 192summary-tags
192 Specifies the number of tags to display in the repository "summary" 193 Specifies the number of tags to display in the repository "summary"
193 view. Default value: "10". 194 view. Default value: "10".
194 195
195virtual-root 196virtual-root
196 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
197 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
198 '/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
199 value: none. 200 value: none.
200 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
201 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.
202 203
203REPOSITORY SETTINGS 204REPOSITORY SETTINGS
204------------------- 205-------------------
205repo.clone-url 206repo.clone-url
206 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.
207 Default value: none. 208 Default value: none.
208 209
209repo.defbranch 210repo.defbranch
210 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
211 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
212 as default instead. Default value: "master". 213 as default instead. Default value: "master".
213 214
214repo.desc 215repo.desc
215 The value to show as repository description. Default value: none. 216 The value to show as repository description. Default value: none.
216 217
217repo.enable-log-filecount 218repo.enable-log-filecount
218 A flag which can be used to disable the global setting 219 A flag which can be used to disable the global setting
219 `enable-log-filecount'. Default value: none. 220 `enable-log-filecount'. Default value: none.
220 221
221repo.enable-log-linecount 222repo.enable-log-linecount
222 A flag which can be used to disable the global setting 223 A flag which can be used to disable the global setting
223 `enable-log-linecount'. Default value: none. 224 `enable-log-linecount'. Default value: none.
224 225
225repo.enable-stats 226repo.max-stats
226 A flag which can be used to disable the global setting 227 Override the default maximum statistics period. Valid values are equal
227 `enable-stats'. Default value: none. 228 to the values specified for the global "max-stats" setting. Default
229 value: none.
228 230
229repo.name 231repo.name
230 The value to show as repository name. Default value: <repo.url>. 232 The value to show as repository name. Default value: <repo.url>.
231 233
232repo.owner 234repo.owner
233 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:
234 none. 236 none.
235 237
236repo.path 238repo.path
237 An absolute path to the repository directory. For non-bare repositories 239 An absolute path to the repository directory. For non-bare repositories
238 this is the .git-directory. Default value: none. 240 this is the .git-directory. Default value: none.
239 241
240repo.readme 242repo.readme
241 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
242 verbatim as the "About" page for this repo. Default value: none. 244 verbatim as the "About" page for this repo. Default value: none.
243 245
244repo.snapshots 246repo.snapshots
245 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
246 "snapshots" global setting. Default value: <snapshots>. 248 "snapshots" global setting. Default value: <snapshots>.
247 249
248repo.url 250repo.url
249 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
250 setting specified for each repo. Default value: none. 252 setting specified for each repo. Default value: none.
251 253
252 254
253EXAMPLE CGITRC FILE 255EXAMPLE CGITRC FILE
254------------------- 256-------------------
255 257
256# Enable caching of up to 1000 output entriess 258# Enable caching of up to 1000 output entriess
257cache-size=1000 259cache-size=1000
258 260
259 261
260# Specify some default clone prefixes 262# Specify some default clone prefixes
261clone-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
262 264
263# Specify the css url 265# Specify the css url
264css=/css/cgit.css 266css=/css/cgit.css
265 267
266 268
267# Show extra links for each repository on the index page 269# Show extra links for each repository on the index page
268enable-index-links=1 270enable-index-links=1
269 271
270 272
271# Show number of affected files per commit on the log pages 273# Show number of affected files per commit on the log pages
272enable-log-filecount=1 274enable-log-filecount=1
273 275
274 276
275# 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
276enable-log-linecount=1 278enable-log-linecount=1
277 279
278 280
279# Add a cgit favicon 281# Add a cgit favicon
280favicon=/favicon.ico 282favicon=/favicon.ico
281 283
282 284
283# Use a custom logo 285# Use a custom logo
284logo=/img/mylogo.png 286logo=/img/mylogo.png
285 287
286 288
289# Enable statistics per week, month and quarter
290max-stats=quarter
291
292
287# Set the title and heading of the repository index page 293# Set the title and heading of the repository index page
288root-title=foobar.com git repositories 294root-title=foobar.com git repositories
289 295
290 296
291# Set a subheading for the repository index page 297# Set a subheading for the repository index page
292root-desc=tracking the foobar development 298root-desc=tracking the foobar development
293 299
294 300
295# Include some more info about foobar.com on the index page 301# Include some more info about foobar.com on the index page
296root-readme=/var/www/htdocs/about.html 302root-readme=/var/www/htdocs/about.html
297 303
298 304
299# Allow download of tar.gz, tar.bz and zip-files 305# Allow download of tar.gz, tar.bz and zip-files
300snapshots=tar.gz tar.bz zip 306snapshots=tar.gz tar.bz zip
301 307
302 308
303## 309##
304## List of repositories. 310## List of repositories.
305## 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
306## displayed under a group heading 312## displayed under a group heading
307## 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')
308## and included like this: 314## and included like this:
309## include=/etc/cgitrepos 315## include=/etc/cgitrepos
310## 316##
311 317
312 318
313repo.url=foo 319repo.url=foo
314repo.path=/pub/git/foo.git 320repo.path=/pub/git/foo.git
315repo.desc=the master foo repository 321repo.desc=the master foo repository
316repo.owner=fooman@foobar.com 322repo.owner=fooman@foobar.com
317repo.readme=info/web/about.html 323repo.readme=info/web/about.html
318 324
319 325
320repo.url=bar 326repo.url=bar
321repo.path=/pub/git/bar.git 327repo.path=/pub/git/bar.git
322repo.desc=the bars for your foo 328repo.desc=the bars for your foo
323repo.owner=barman@foobar.com 329repo.owner=barman@foobar.com
324repo.readme=info/web/about.html 330repo.readme=info/web/about.html
325 331
326 332
327# The next repositories will be displayed under the 'extras' heading 333# The next repositories will be displayed under the 'extras' heading
328repo.group=extras 334repo.group=extras
329 335
330 336
331repo.url=baz 337repo.url=baz
332repo.path=/pub/git/baz.git 338repo.path=/pub/git/baz.git
333repo.desc=a set of extensions for bar users 339repo.desc=a set of extensions for bar users
334 340
335repo.url=wiz 341repo.url=wiz
336repo.path=/pub/git/wiz.git 342repo.path=/pub/git/wiz.git
337repo.desc=the wizard of foo 343repo.desc=the wizard of foo
338 344
339 345
340# Add some mirrored repositories 346# Add some mirrored repositories
341repo.group=mirrors 347repo.group=mirrors
342 348
343 349
344repo.url=git 350repo.url=git
345repo.path=/pub/git/git.git 351repo.path=/pub/git/git.git
346repo.desc=the dscm 352repo.desc=the dscm
347 353
348 354
349repo.url=linux 355repo.url=linux
350repo.path=/pub/git/linux.git 356repo.path=/pub/git/linux.git
351repo.desc=the kernel 357repo.desc=the kernel
352 358
353# Disable adhoc downloads of this repo 359# Disable adhoc downloads of this repo
354repo.snapshots=0 360repo.snapshots=0
355 361
356# Disable line-counts for this repo 362# Disable line-counts for this repo
357repo.enable-log-linecount=0 363repo.enable-log-linecount=0
358 364
365# Restrict the max statistics period for this repo
366repo.max-stats=month
367
359 368
360BUGS 369BUGS
361---- 370----
362Comments 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
363will be included as part of the value. E.g. this line: 372will be included as part of the value. E.g. this line:
364 373
365 robots=index # allow indexing 374 robots=index # allow indexing
366 375
367will generate the following html element: 376will generate the following html element:
368 377
369 <meta name='robots' content='index # allow indexing'/> 378 <meta name='robots' content='index # allow indexing'/>
370 379
371 380
372 381
373AUTHOR 382AUTHOR
374------ 383------
375Lars Hjemli <hjemli@gmail.com> 384Lars Hjemli <hjemli@gmail.com>
diff --git a/cmd.c b/cmd.c
index 744bf84..763a558 100644
--- a/cmd.c
+++ b/cmd.c
@@ -67,100 +67,97 @@ static void info_fn(struct cgit_context *ctx)
67static void log_fn(struct cgit_context *ctx) 67static void log_fn(struct cgit_context *ctx)
68{ 68{
69 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,
70 ctx->qry.grep, ctx->qry.search, ctx->qry.path, 1); 70 ctx->qry.grep, ctx->qry.search, ctx->qry.path, 1);
71} 71}
72 72
73static void ls_cache_fn(struct cgit_context *ctx) 73static void ls_cache_fn(struct cgit_context *ctx)
74{ 74{
75 ctx->page.mimetype = "text/plain"; 75 ctx->page.mimetype = "text/plain";
76 ctx->page.filename = "ls-cache.txt"; 76 ctx->page.filename = "ls-cache.txt";
77 cgit_print_http_headers(ctx); 77 cgit_print_http_headers(ctx);
78 cache_ls(ctx->cfg.cache_root); 78 cache_ls(ctx->cfg.cache_root);
79} 79}
80 80
81static void objects_fn(struct cgit_context *ctx) 81static void objects_fn(struct cgit_context *ctx)
82{ 82{
83 cgit_clone_objects(ctx); 83 cgit_clone_objects(ctx);
84} 84}
85 85
86static void repolist_fn(struct cgit_context *ctx) 86static void repolist_fn(struct cgit_context *ctx)
87{ 87{
88 cgit_print_repolist(); 88 cgit_print_repolist();
89} 89}
90 90
91static void patch_fn(struct cgit_context *ctx) 91static void patch_fn(struct cgit_context *ctx)
92{ 92{
93 cgit_print_patch(ctx->qry.sha1); 93 cgit_print_patch(ctx->qry.sha1);
94} 94}
95 95
96static void plain_fn(struct cgit_context *ctx) 96static void plain_fn(struct cgit_context *ctx)
97{ 97{
98 cgit_print_plain(ctx); 98 cgit_print_plain(ctx);
99} 99}
100 100
101static void refs_fn(struct cgit_context *ctx) 101static void refs_fn(struct cgit_context *ctx)
102{ 102{
103 cgit_print_refs(); 103 cgit_print_refs();
104} 104}
105 105
106static void snapshot_fn(struct cgit_context *ctx) 106static void snapshot_fn(struct cgit_context *ctx)
107{ 107{
108 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, 108 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1,
109 cgit_repobasename(ctx->repo->url), ctx->qry.path, 109 cgit_repobasename(ctx->repo->url), ctx->qry.path,
110 ctx->repo->snapshots, ctx->qry.nohead); 110 ctx->repo->snapshots, ctx->qry.nohead);
111} 111}
112 112
113static void stats_fn(struct cgit_context *ctx) 113static void stats_fn(struct cgit_context *ctx)
114{ 114{
115 if (ctx->repo->enable_stats) 115 cgit_show_stats(ctx);
116 cgit_show_stats(ctx);
117 else
118 cgit_print_error("Stats disabled for this repo");
119} 116}
120 117
121static void summary_fn(struct cgit_context *ctx) 118static void summary_fn(struct cgit_context *ctx)
122{ 119{
123 cgit_print_summary(); 120 cgit_print_summary();
124} 121}
125 122
126static void tag_fn(struct cgit_context *ctx) 123static void tag_fn(struct cgit_context *ctx)
127{ 124{
128 cgit_print_tag(ctx->qry.sha1); 125 cgit_print_tag(ctx->qry.sha1);
129} 126}
130 127
131static void tree_fn(struct cgit_context *ctx) 128static void tree_fn(struct cgit_context *ctx)
132{ 129{
133 cgit_print_tree(ctx->qry.sha1, ctx->qry.path); 130 cgit_print_tree(ctx->qry.sha1, ctx->qry.path);
134} 131}
135 132
136#define def_cmd(name, want_repo, want_layout) \ 133#define def_cmd(name, want_repo, want_layout) \
137 {#name, name##_fn, want_repo, want_layout} 134 {#name, name##_fn, want_repo, want_layout}
138 135
139struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx) 136struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx)
140{ 137{
141 static struct cgit_cmd cmds[] = { 138 static struct cgit_cmd cmds[] = {
142 def_cmd(HEAD, 1, 0), 139 def_cmd(HEAD, 1, 0),
143 def_cmd(atom, 1, 0), 140 def_cmd(atom, 1, 0),
144 def_cmd(about, 0, 1), 141 def_cmd(about, 0, 1),
145 def_cmd(blob, 1, 0), 142 def_cmd(blob, 1, 0),
146 def_cmd(commit, 1, 1), 143 def_cmd(commit, 1, 1),
147 def_cmd(diff, 1, 1), 144 def_cmd(diff, 1, 1),
148 def_cmd(info, 1, 0), 145 def_cmd(info, 1, 0),
149 def_cmd(log, 1, 1), 146 def_cmd(log, 1, 1),
150 def_cmd(ls_cache, 0, 0), 147 def_cmd(ls_cache, 0, 0),
151 def_cmd(objects, 1, 0), 148 def_cmd(objects, 1, 0),
152 def_cmd(patch, 1, 0), 149 def_cmd(patch, 1, 0),
153 def_cmd(plain, 1, 0), 150 def_cmd(plain, 1, 0),
154 def_cmd(refs, 1, 1), 151 def_cmd(refs, 1, 1),
155 def_cmd(repolist, 0, 0), 152 def_cmd(repolist, 0, 0),
156 def_cmd(snapshot, 1, 0), 153 def_cmd(snapshot, 1, 0),
157 def_cmd(stats, 1, 1), 154 def_cmd(stats, 1, 1),
158 def_cmd(summary, 1, 1), 155 def_cmd(summary, 1, 1),
159 def_cmd(tag, 1, 1), 156 def_cmd(tag, 1, 1),
160 def_cmd(tree, 1, 1), 157 def_cmd(tree, 1, 1),
161 }; 158 };
162 int i; 159 int i;
163 160
164 if (ctx->qry.page == NULL) { 161 if (ctx->qry.page == NULL) {
165 if (ctx->repo) 162 if (ctx->repo)
166 ctx->qry.page = "summary"; 163 ctx->qry.page = "summary";
diff --git a/shared.c b/shared.c
index 37333f0..7382609 100644
--- a/shared.c
+++ b/shared.c
@@ -13,97 +13,97 @@ struct cgit_context ctx;
13int cgit_cmd; 13int cgit_cmd;
14 14
15int chk_zero(int result, char *msg) 15int chk_zero(int result, char *msg)
16{ 16{
17 if (result != 0) 17 if (result != 0)
18 die("%s: %s", msg, strerror(errno)); 18 die("%s: %s", msg, strerror(errno));
19 return result; 19 return result;
20} 20}
21 21
22int chk_positive(int result, char *msg) 22int chk_positive(int result, char *msg)
23{ 23{
24 if (result <= 0) 24 if (result <= 0)
25 die("%s: %s", msg, strerror(errno)); 25 die("%s: %s", msg, strerror(errno));
26 return result; 26 return result;
27} 27}
28 28
29int chk_non_negative(int result, char *msg) 29int chk_non_negative(int result, char *msg)
30{ 30{
31 if (result < 0) 31 if (result < 0)
32 die("%s: %s",msg, strerror(errno)); 32 die("%s: %s",msg, strerror(errno));
33 return result; 33 return result;
34} 34}
35 35
36struct cgit_repo *cgit_add_repo(const char *url) 36struct cgit_repo *cgit_add_repo(const char *url)
37{ 37{
38 struct cgit_repo *ret; 38 struct cgit_repo *ret;
39 39
40 if (++cgit_repolist.count > cgit_repolist.length) { 40 if (++cgit_repolist.count > cgit_repolist.length) {
41 if (cgit_repolist.length == 0) 41 if (cgit_repolist.length == 0)
42 cgit_repolist.length = 8; 42 cgit_repolist.length = 8;
43 else 43 else
44 cgit_repolist.length *= 2; 44 cgit_repolist.length *= 2;
45 cgit_repolist.repos = xrealloc(cgit_repolist.repos, 45 cgit_repolist.repos = xrealloc(cgit_repolist.repos,
46 cgit_repolist.length * 46 cgit_repolist.length *
47 sizeof(struct cgit_repo)); 47 sizeof(struct cgit_repo));
48 } 48 }
49 49
50 ret = &cgit_repolist.repos[cgit_repolist.count-1]; 50 ret = &cgit_repolist.repos[cgit_repolist.count-1];
51 ret->url = trim_end(url, '/'); 51 ret->url = trim_end(url, '/');
52 ret->name = ret->url; 52 ret->name = ret->url;
53 ret->path = NULL; 53 ret->path = NULL;
54 ret->desc = "[no description]"; 54 ret->desc = "[no description]";
55 ret->owner = NULL; 55 ret->owner = NULL;
56 ret->group = ctx.cfg.repo_group; 56 ret->group = ctx.cfg.repo_group;
57 ret->defbranch = "master"; 57 ret->defbranch = "master";
58 ret->snapshots = ctx.cfg.snapshots; 58 ret->snapshots = ctx.cfg.snapshots;
59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount; 59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount; 60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
61 ret->enable_stats = ctx.cfg.enable_stats; 61 ret->max_stats = ctx.cfg.max_stats;
62 ret->module_link = ctx.cfg.module_link; 62 ret->module_link = ctx.cfg.module_link;
63 ret->readme = NULL; 63 ret->readme = NULL;
64 return ret; 64 return ret;
65} 65}
66 66
67struct cgit_repo *cgit_get_repoinfo(const char *url) 67struct cgit_repo *cgit_get_repoinfo(const char *url)
68{ 68{
69 int i; 69 int i;
70 struct cgit_repo *repo; 70 struct cgit_repo *repo;
71 71
72 for (i=0; i<cgit_repolist.count; i++) { 72 for (i=0; i<cgit_repolist.count; i++) {
73 repo = &cgit_repolist.repos[i]; 73 repo = &cgit_repolist.repos[i];
74 if (!strcmp(repo->url, url)) 74 if (!strcmp(repo->url, url))
75 return repo; 75 return repo;
76 } 76 }
77 return NULL; 77 return NULL;
78} 78}
79 79
80void *cgit_free_commitinfo(struct commitinfo *info) 80void *cgit_free_commitinfo(struct commitinfo *info)
81{ 81{
82 free(info->author); 82 free(info->author);
83 free(info->author_email); 83 free(info->author_email);
84 free(info->committer); 84 free(info->committer);
85 free(info->committer_email); 85 free(info->committer_email);
86 free(info->subject); 86 free(info->subject);
87 free(info->msg); 87 free(info->msg);
88 free(info->msg_encoding); 88 free(info->msg_encoding);
89 free(info); 89 free(info);
90 return NULL; 90 return NULL;
91} 91}
92 92
93char *trim_end(const char *str, char c) 93char *trim_end(const char *str, char c)
94{ 94{
95 int len; 95 int len;
96 char *s, *t; 96 char *s, *t;
97 97
98 if (str == NULL) 98 if (str == NULL)
99 return NULL; 99 return NULL;
100 t = (char *)str; 100 t = (char *)str;
101 len = strlen(t); 101 len = strlen(t);
102 while(len > 0 && t[len - 1] == c) 102 while(len > 0 && t[len - 1] == c)
103 len--; 103 len--;
104 104
105 if (len == 0) 105 if (len == 0)
106 return NULL; 106 return NULL;
107 107
108 c = t[len]; 108 c = t[len];
109 t[len] = '\0'; 109 t[len] = '\0';
diff --git a/ui-shared.c b/ui-shared.c
index 0e688a0..97b9d46 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -596,97 +596,97 @@ void cgit_print_pageheader(struct cgit_context *ctx)
596 html("'><img src='"); 596 html("'><img src='");
597 html_attr(ctx->cfg.logo); 597 html_attr(ctx->cfg.logo);
598 html("' alt='cgit logo'/></a></td>\n"); 598 html("' alt='cgit logo'/></a></td>\n");
599 599
600 html("<td class='main'>"); 600 html("<td class='main'>");
601 if (ctx->repo) { 601 if (ctx->repo) {
602 cgit_index_link("index", NULL, NULL, NULL, 0); 602 cgit_index_link("index", NULL, NULL, NULL, 0);
603 html(" : "); 603 html(" : ");
604 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); 604 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
605 html("</td><td class='form'>"); 605 html("</td><td class='form'>");
606 html("<form method='get' action=''>\n"); 606 html("<form method='get' action=''>\n");
607 add_hidden_formfields(0, 1, ctx->qry.page); 607 add_hidden_formfields(0, 1, ctx->qry.page);
608 html("<select name='h' onchange='this.form.submit();'>\n"); 608 html("<select name='h' onchange='this.form.submit();'>\n");
609 for_each_branch_ref(print_branch_option, ctx->qry.head); 609 for_each_branch_ref(print_branch_option, ctx->qry.head);
610 html("</select> "); 610 html("</select> ");
611 html("<input type='submit' name='' value='switch'/>"); 611 html("<input type='submit' name='' value='switch'/>");
612 html("</form>"); 612 html("</form>");
613 } else 613 } else
614 html_txt(ctx->cfg.root_title); 614 html_txt(ctx->cfg.root_title);
615 html("</td></tr>\n"); 615 html("</td></tr>\n");
616 616
617 html("<tr><td class='sub'>"); 617 html("<tr><td class='sub'>");
618 if (ctx->repo) { 618 if (ctx->repo) {
619 html_txt(ctx->repo->desc); 619 html_txt(ctx->repo->desc);
620 html("</td><td class='sub right'>"); 620 html("</td><td class='sub right'>");
621 html_txt(ctx->repo->owner); 621 html_txt(ctx->repo->owner);
622 } else { 622 } else {
623 if (ctx->cfg.root_desc) 623 if (ctx->cfg.root_desc)
624 html_txt(ctx->cfg.root_desc); 624 html_txt(ctx->cfg.root_desc);
625 else if (ctx->cfg.index_info) 625 else if (ctx->cfg.index_info)
626 html_include(ctx->cfg.index_info); 626 html_include(ctx->cfg.index_info);
627 } 627 }
628 html("</td></tr></table>\n"); 628 html("</td></tr></table>\n");
629 629
630 html("<table class='tabs'><tr><td>\n"); 630 html("<table class='tabs'><tr><td>\n");
631 if (ctx->repo) { 631 if (ctx->repo) {
632 cgit_summary_link("summary", NULL, hc(cmd, "summary"), 632 cgit_summary_link("summary", NULL, hc(cmd, "summary"),
633 ctx->qry.head); 633 ctx->qry.head);
634 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head, 634 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head,
635 ctx->qry.sha1, NULL); 635 ctx->qry.sha1, NULL);
636 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head, 636 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head,
637 NULL, NULL, 0, NULL, NULL); 637 NULL, NULL, 0, NULL, NULL);
638 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 638 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
639 ctx->qry.sha1, NULL); 639 ctx->qry.sha1, NULL);
640 cgit_commit_link("commit", NULL, hc(cmd, "commit"), 640 cgit_commit_link("commit", NULL, hc(cmd, "commit"),
641 ctx->qry.head, ctx->qry.sha1); 641 ctx->qry.head, ctx->qry.sha1);
642 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 642 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
643 ctx->qry.sha1, ctx->qry.sha2, NULL); 643 ctx->qry.sha1, ctx->qry.sha2, NULL);
644 if (ctx->repo->enable_stats) 644 if (ctx->repo->max_stats)
645 reporevlink("stats", "stats", NULL, hc(cmd, "stats"), 645 reporevlink("stats", "stats", NULL, hc(cmd, "stats"),
646 ctx->qry.head, NULL, NULL); 646 ctx->qry.head, NULL, NULL);
647 if (ctx->repo->readme) 647 if (ctx->repo->readme)
648 reporevlink("about", "about", NULL, 648 reporevlink("about", "about", NULL,
649 hc(cmd, "about"), ctx->qry.head, NULL, 649 hc(cmd, "about"), ctx->qry.head, NULL,
650 NULL); 650 NULL);
651 html("</td><td class='form'>"); 651 html("</td><td class='form'>");
652 html("<form class='right' method='get' action='"); 652 html("<form class='right' method='get' action='");
653 if (ctx->cfg.virtual_root) 653 if (ctx->cfg.virtual_root)
654 html_url_path(cgit_fileurl(ctx->qry.repo, "log", 654 html_url_path(cgit_fileurl(ctx->qry.repo, "log",
655 ctx->qry.path, NULL)); 655 ctx->qry.path, NULL));
656 html("'>\n"); 656 html("'>\n");
657 add_hidden_formfields(1, 0, "log"); 657 add_hidden_formfields(1, 0, "log");
658 html("<select name='qt'>\n"); 658 html("<select name='qt'>\n");
659 html_option("grep", "log msg", ctx->qry.grep); 659 html_option("grep", "log msg", ctx->qry.grep);
660 html_option("author", "author", ctx->qry.grep); 660 html_option("author", "author", ctx->qry.grep);
661 html_option("committer", "committer", ctx->qry.grep); 661 html_option("committer", "committer", ctx->qry.grep);
662 html("</select>\n"); 662 html("</select>\n");
663 html("<input class='txt' type='text' size='10' name='q' value='"); 663 html("<input class='txt' type='text' size='10' name='q' value='");
664 html_attr(ctx->qry.search); 664 html_attr(ctx->qry.search);
665 html("'/>\n"); 665 html("'/>\n");
666 html("<input type='submit' value='search'/>\n"); 666 html("<input type='submit' value='search'/>\n");
667 html("</form>\n"); 667 html("</form>\n");
668 } else { 668 } else {
669 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0); 669 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0);
670 if (ctx->cfg.root_readme) 670 if (ctx->cfg.root_readme)
671 site_link("about", "about", NULL, hc(cmd, "about"), 671 site_link("about", "about", NULL, hc(cmd, "about"),
672 NULL, 0); 672 NULL, 0);
673 html("</td><td class='form'>"); 673 html("</td><td class='form'>");
674 html("<form method='get' action='"); 674 html("<form method='get' action='");
675 html_attr(cgit_rooturl()); 675 html_attr(cgit_rooturl());
676 html("'>\n"); 676 html("'>\n");
677 html("<input type='text' name='q' size='10' value='"); 677 html("<input type='text' name='q' size='10' value='");
678 html_attr(ctx->qry.search); 678 html_attr(ctx->qry.search);
679 html("'/>\n"); 679 html("'/>\n");
680 html("<input type='submit' value='search'/>\n"); 680 html("<input type='submit' value='search'/>\n");
681 html("</form>"); 681 html("</form>");
682 } 682 }
683 html("</td></tr></table>\n"); 683 html("</td></tr></table>\n");
684 html("<div class='content'>"); 684 html("<div class='content'>");
685} 685}
686 686
687void cgit_print_filemode(unsigned short mode) 687void cgit_print_filemode(unsigned short mode)
688{ 688{
689 if (S_ISDIR(mode)) 689 if (S_ISDIR(mode))
690 html("d"); 690 html("d");
691 else if (S_ISLNK(mode)) 691 else if (S_ISLNK(mode))
692 html("l"); 692 html("l");
diff --git a/ui-stats.c b/ui-stats.c
index 3cc8d70..1104485 100644
--- a/ui-stats.c
+++ b/ui-stats.c
@@ -1,71 +1,57 @@
1#include <string-list.h>
2
1#include "cgit.h" 3#include "cgit.h"
2#include "html.h" 4#include "html.h"
3#include <string-list.h> 5#include "ui-shared.h"
6#include "ui-stats.h"
4 7
5#define MONTHS 6 8#define MONTHS 6
6 9
7struct Period {
8 const char code;
9 const char *name;
10 int max_periods;
11 int count;
12
13 /* Convert a tm value to the first day in the period */
14 void (*trunc)(struct tm *tm);
15
16 /* Update tm value to start of next/previous period */
17 void (*dec)(struct tm *tm);
18 void (*inc)(struct tm *tm);
19
20 /* Pretty-print a tm value */
21 char *(*pretty)(struct tm *tm);
22};
23
24struct authorstat { 10struct authorstat {
25 long total; 11 long total;
26 struct string_list list; 12 struct string_list list;
27}; 13};
28 14
29#define DAY_SECS (60 * 60 * 24) 15#define DAY_SECS (60 * 60 * 24)
30#define WEEK_SECS (DAY_SECS * 7) 16#define WEEK_SECS (DAY_SECS * 7)
31 17
32static void trunc_week(struct tm *tm) 18static void trunc_week(struct tm *tm)
33{ 19{
34 time_t t = timegm(tm); 20 time_t t = timegm(tm);
35 t -= ((tm->tm_wday + 6) % 7) * DAY_SECS; 21 t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
36 gmtime_r(&t, tm); 22 gmtime_r(&t, tm);
37} 23}
38 24
39static void dec_week(struct tm *tm) 25static void dec_week(struct tm *tm)
40{ 26{
41 time_t t = timegm(tm); 27 time_t t = timegm(tm);
42 t -= WEEK_SECS; 28 t -= WEEK_SECS;
43 gmtime_r(&t, tm); 29 gmtime_r(&t, tm);
44} 30}
45 31
46static void inc_week(struct tm *tm) 32static void inc_week(struct tm *tm)
47{ 33{
48 time_t t = timegm(tm); 34 time_t t = timegm(tm);
49 t += WEEK_SECS; 35 t += WEEK_SECS;
50 gmtime_r(&t, tm); 36 gmtime_r(&t, tm);
51} 37}
52 38
53static char *pretty_week(struct tm *tm) 39static char *pretty_week(struct tm *tm)
54{ 40{
55 static char buf[10]; 41 static char buf[10];
56 42
57 strftime(buf, sizeof(buf), "W%V %G", tm); 43 strftime(buf, sizeof(buf), "W%V %G", tm);
58 return buf; 44 return buf;
59} 45}
60 46
61static void trunc_month(struct tm *tm) 47static void trunc_month(struct tm *tm)
62{ 48{
63 tm->tm_mday = 1; 49 tm->tm_mday = 1;
64} 50}
65 51
66static void dec_month(struct tm *tm) 52static void dec_month(struct tm *tm)
67{ 53{
68 tm->tm_mon--; 54 tm->tm_mon--;
69 if (tm->tm_mon < 0) { 55 if (tm->tm_mon < 0) {
70 tm->tm_year--; 56 tm->tm_year--;
71 tm->tm_mon = 11; 57 tm->tm_mon = 11;
@@ -92,301 +78,334 @@ static char *pretty_month(struct tm *tm)
92 78
93static void trunc_quarter(struct tm *tm) 79static void trunc_quarter(struct tm *tm)
94{ 80{
95 trunc_month(tm); 81 trunc_month(tm);
96 while(tm->tm_mon % 3 != 0) 82 while(tm->tm_mon % 3 != 0)
97 dec_month(tm); 83 dec_month(tm);
98} 84}
99 85
100static void dec_quarter(struct tm *tm) 86static void dec_quarter(struct tm *tm)
101{ 87{
102 dec_month(tm); 88 dec_month(tm);
103 dec_month(tm); 89 dec_month(tm);
104 dec_month(tm); 90 dec_month(tm);
105} 91}
106 92
107static void inc_quarter(struct tm *tm) 93static void inc_quarter(struct tm *tm)
108{ 94{
109 inc_month(tm); 95 inc_month(tm);
110 inc_month(tm); 96 inc_month(tm);
111 inc_month(tm); 97 inc_month(tm);
112} 98}
113 99
114static char *pretty_quarter(struct tm *tm) 100static char *pretty_quarter(struct tm *tm)
115{ 101{
116 return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900); 102 return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
117} 103}
118 104
119static void trunc_year(struct tm *tm) 105static void trunc_year(struct tm *tm)
120{ 106{
121 trunc_month(tm); 107 trunc_month(tm);
122 tm->tm_mon = 0; 108 tm->tm_mon = 0;
123} 109}
124 110
125static void dec_year(struct tm *tm) 111static void dec_year(struct tm *tm)
126{ 112{
127 tm->tm_year--; 113 tm->tm_year--;
128} 114}
129 115
130static void inc_year(struct tm *tm) 116static void inc_year(struct tm *tm)
131{ 117{
132 tm->tm_year++; 118 tm->tm_year++;
133} 119}
134 120
135static char *pretty_year(struct tm *tm) 121static char *pretty_year(struct tm *tm)
136{ 122{
137 return fmt("%d", tm->tm_year + 1900); 123 return fmt("%d", tm->tm_year + 1900);
138} 124}
139 125
140struct Period periods[] = { 126struct cgit_period periods[] = {
141 {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week}, 127 {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
142 {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month}, 128 {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
143 {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter}, 129 {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
144 {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year}, 130 {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
145}; 131};
146 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
147static void add_commit(struct string_list *authors, struct commit *commit, 157static void add_commit(struct string_list *authors, struct commit *commit,
148 struct Period *period) 158 struct cgit_period *period)
149{ 159{
150 struct commitinfo *info; 160 struct commitinfo *info;
151 struct string_list_item *author, *item; 161 struct string_list_item *author, *item;
152 struct authorstat *authorstat; 162 struct authorstat *authorstat;
153 struct string_list *items; 163 struct string_list *items;
154 char *tmp; 164 char *tmp;
155 struct tm *date; 165 struct tm *date;
156 time_t t; 166 time_t t;
157 167
158 info = cgit_parse_commit(commit); 168 info = cgit_parse_commit(commit);
159 tmp = xstrdup(info->author); 169 tmp = xstrdup(info->author);
160 author = string_list_insert(tmp, authors); 170 author = string_list_insert(tmp, authors);
161 if (!author->util) 171 if (!author->util)
162 author->util = xcalloc(1, sizeof(struct authorstat)); 172 author->util = xcalloc(1, sizeof(struct authorstat));
163 else 173 else
164 free(tmp); 174 free(tmp);
165 authorstat = author->util; 175 authorstat = author->util;
166 items = &authorstat->list; 176 items = &authorstat->list;
167 t = info->committer_date; 177 t = info->committer_date;
168 date = gmtime(&t); 178 date = gmtime(&t);
169 period->trunc(date); 179 period->trunc(date);
170 tmp = xstrdup(period->pretty(date)); 180 tmp = xstrdup(period->pretty(date));
171 item = string_list_insert(tmp, items); 181 item = string_list_insert(tmp, items);
172 if (item->util) 182 if (item->util)
173 free(tmp); 183 free(tmp);
174 item->util++; 184 item->util++;
175 authorstat->total++; 185 authorstat->total++;
176 cgit_free_commitinfo(info); 186 cgit_free_commitinfo(info);
177} 187}
178 188
179static int cmp_total_commits(const void *a1, const void *a2) 189static int cmp_total_commits(const void *a1, const void *a2)
180{ 190{
181 const struct string_list_item *i1 = a1; 191 const struct string_list_item *i1 = a1;
182 const struct string_list_item *i2 = a2; 192 const struct string_list_item *i2 = a2;
183 const struct authorstat *auth1 = i1->util; 193 const struct authorstat *auth1 = i1->util;
184 const struct authorstat *auth2 = i2->util; 194 const struct authorstat *auth2 = i2->util;
185 195
186 return auth2->total - auth1->total; 196 return auth2->total - auth1->total;
187} 197}
188 198
189/* Walk the commit DAG and collect number of commits per author per 199/* Walk the commit DAG and collect number of commits per author per
190 * timeperiod into a nested string_list collection. 200 * timeperiod into a nested string_list collection.
191 */ 201 */
192struct string_list collect_stats(struct cgit_context *ctx, 202struct string_list collect_stats(struct cgit_context *ctx,
193 struct Period *period) 203 struct cgit_period *period)
194{ 204{
195 struct string_list authors; 205 struct string_list authors;
196 struct rev_info rev; 206 struct rev_info rev;
197 struct commit *commit; 207 struct commit *commit;
198 const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL}; 208 const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL};
199 int argc = 3; 209 int argc = 3;
200 time_t now; 210 time_t now;
201 long i; 211 long i;
202 struct tm *tm; 212 struct tm *tm;
203 char tmp[11]; 213 char tmp[11];
204 214
205 time(&now); 215 time(&now);
206 tm = gmtime(&now); 216 tm = gmtime(&now);
207 period->trunc(tm); 217 period->trunc(tm);
208 for (i = 1; i < period->count; i++) 218 for (i = 1; i < period->count; i++)
209 period->dec(tm); 219 period->dec(tm);
210 strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm); 220 strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
211 argv[2] = xstrdup(fmt("--since=%s", tmp)); 221 argv[2] = xstrdup(fmt("--since=%s", tmp));
212 if (ctx->qry.path) { 222 if (ctx->qry.path) {
213 argv[3] = "--"; 223 argv[3] = "--";
214 argv[4] = ctx->qry.path; 224 argv[4] = ctx->qry.path;
215 argc += 2; 225 argc += 2;
216 } 226 }
217 init_revisions(&rev, NULL); 227 init_revisions(&rev, NULL);
218 rev.abbrev = DEFAULT_ABBREV; 228 rev.abbrev = DEFAULT_ABBREV;
219 rev.commit_format = CMIT_FMT_DEFAULT; 229 rev.commit_format = CMIT_FMT_DEFAULT;
220 rev.no_merges = 1; 230 rev.no_merges = 1;
221 rev.verbose_header = 1; 231 rev.verbose_header = 1;
222 rev.show_root_diff = 0; 232 rev.show_root_diff = 0;
223 setup_revisions(argc, argv, &rev, NULL); 233 setup_revisions(argc, argv, &rev, NULL);
224 prepare_revision_walk(&rev); 234 prepare_revision_walk(&rev);
225 memset(&authors, 0, sizeof(authors)); 235 memset(&authors, 0, sizeof(authors));
226 while ((commit = get_revision(&rev)) != NULL) { 236 while ((commit = get_revision(&rev)) != NULL) {
227 add_commit(&authors, commit, period); 237 add_commit(&authors, commit, period);
228 free(commit->buffer); 238 free(commit->buffer);
229 free_commit_list(commit->parents); 239 free_commit_list(commit->parents);
230 } 240 }
231 return authors; 241 return authors;
232} 242}
233 243
234void print_combined_authorrow(struct string_list *authors, int from, int to, 244void print_combined_authorrow(struct string_list *authors, int from, int to,
235 const char *name, const char *leftclass, const char *centerclass, 245 const char *name, const char *leftclass, const char *centerclass,
236 const char *rightclass, struct Period *period) 246 const char *rightclass, struct cgit_period *period)
237{ 247{
238 struct string_list_item *author; 248 struct string_list_item *author;
239 struct authorstat *authorstat; 249 struct authorstat *authorstat;
240 struct string_list *items; 250 struct string_list *items;
241 struct string_list_item *date; 251 struct string_list_item *date;
242 time_t now; 252 time_t now;
243 long i, j, total, subtotal; 253 long i, j, total, subtotal;
244 struct tm *tm; 254 struct tm *tm;
245 char *tmp; 255 char *tmp;
246 256
247 time(&now); 257 time(&now);
248 tm = gmtime(&now); 258 tm = gmtime(&now);
249 period->trunc(tm); 259 period->trunc(tm);
250 for (i = 1; i < period->count; i++) 260 for (i = 1; i < period->count; i++)
251 period->dec(tm); 261 period->dec(tm);
252 262
253 total = 0; 263 total = 0;
254 htmlf("<tr><td class='%s'>%s</td>", leftclass, 264 htmlf("<tr><td class='%s'>%s</td>", leftclass,
255 fmt(name, to - from + 1)); 265 fmt(name, to - from + 1));
256 for (j = 0; j < period->count; j++) { 266 for (j = 0; j < period->count; j++) {
257 tmp = period->pretty(tm); 267 tmp = period->pretty(tm);
258 period->inc(tm); 268 period->inc(tm);
259 subtotal = 0; 269 subtotal = 0;
260 for (i = from; i <= to; i++) { 270 for (i = from; i <= to; i++) {
261 author = &authors->items[i]; 271 author = &authors->items[i];
262 authorstat = author->util; 272 authorstat = author->util;
263 items = &authorstat->list; 273 items = &authorstat->list;
264 date = string_list_lookup(tmp, items); 274 date = string_list_lookup(tmp, items);
265 if (date) 275 if (date)
266 subtotal += (size_t)date->util; 276 subtotal += (size_t)date->util;
267 } 277 }
268 htmlf("<td class='%s'>%d</td>", centerclass, subtotal); 278 htmlf("<td class='%s'>%d</td>", centerclass, subtotal);
269 total += subtotal; 279 total += subtotal;
270 } 280 }
271 htmlf("<td class='%s'>%d</td></tr>", rightclass, total); 281 htmlf("<td class='%s'>%d</td></tr>", rightclass, total);
272} 282}
273 283
274void print_authors(struct string_list *authors, int top, struct Period *period) 284void print_authors(struct string_list *authors, int top,
285 struct cgit_period *period)
275{ 286{
276 struct string_list_item *author; 287 struct string_list_item *author;
277 struct authorstat *authorstat; 288 struct authorstat *authorstat;
278 struct string_list *items; 289 struct string_list *items;
279 struct string_list_item *date; 290 struct string_list_item *date;
280 time_t now; 291 time_t now;
281 long i, j, total; 292 long i, j, total;
282 struct tm *tm; 293 struct tm *tm;
283 char *tmp; 294 char *tmp;
284 295
285 time(&now); 296 time(&now);
286 tm = gmtime(&now); 297 tm = gmtime(&now);
287 period->trunc(tm); 298 period->trunc(tm);
288 for (i = 1; i < period->count; i++) 299 for (i = 1; i < period->count; i++)
289 period->dec(tm); 300 period->dec(tm);
290 301
291 html("<table class='stats'><tr><th>Author</th>"); 302 html("<table class='stats'><tr><th>Author</th>");
292 for (j = 0; j < period->count; j++) { 303 for (j = 0; j < period->count; j++) {
293 tmp = period->pretty(tm); 304 tmp = period->pretty(tm);
294 htmlf("<th>%s</th>", tmp); 305 htmlf("<th>%s</th>", tmp);
295 period->inc(tm); 306 period->inc(tm);
296 } 307 }
297 html("<th>Total</th></tr>\n"); 308 html("<th>Total</th></tr>\n");
298 309
299 if (top <= 0 || top > authors->nr) 310 if (top <= 0 || top > authors->nr)
300 top = authors->nr; 311 top = authors->nr;
301 312
302 for (i = 0; i < top; i++) { 313 for (i = 0; i < top; i++) {
303 author = &authors->items[i]; 314 author = &authors->items[i];
304 html("<tr><td class='left'>"); 315 html("<tr><td class='left'>");
305 html_txt(author->string); 316 html_txt(author->string);
306 html("</td>"); 317 html("</td>");
307 authorstat = author->util; 318 authorstat = author->util;
308 items = &authorstat->list; 319 items = &authorstat->list;
309 total = 0; 320 total = 0;
310 for (j = 0; j < period->count; j++) 321 for (j = 0; j < period->count; j++)
311 period->dec(tm); 322 period->dec(tm);
312 for (j = 0; j < period->count; j++) { 323 for (j = 0; j < period->count; j++) {
313 tmp = period->pretty(tm); 324 tmp = period->pretty(tm);
314 period->inc(tm); 325 period->inc(tm);
315 date = string_list_lookup(tmp, items); 326 date = string_list_lookup(tmp, items);
316 if (!date) 327 if (!date)
317 html("<td>0</td>"); 328 html("<td>0</td>");
318 else { 329 else {
319 htmlf("<td>%d</td>", date->util); 330 htmlf("<td>%d</td>", date->util);
320 total += (size_t)date->util; 331 total += (size_t)date->util;
321 } 332 }
322 } 333 }
323 htmlf("<td class='sum'>%d</td></tr>", total); 334 htmlf("<td class='sum'>%d</td></tr>", total);
324 } 335 }
325 336
326 if (top < authors->nr) 337 if (top < authors->nr)
327 print_combined_authorrow(authors, top, authors->nr - 1, 338 print_combined_authorrow(authors, top, authors->nr - 1,
328 "Others (%d)", "left", "", "sum", period); 339 "Others (%d)", "left", "", "sum", period);
329 340
330 print_combined_authorrow(authors, 0, authors->nr - 1, "Total", 341 print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
331 "total", "sum", "sum", period); 342 "total", "sum", "sum", period);
332 html("</table>"); 343 html("</table>");
333} 344}
334 345
335/* Create a sorted string_list with one entry per author. The util-field 346/* Create a sorted string_list with one entry per author. The util-field
336 * for each author is another string_list which is used to calculate the 347 * for each author is another string_list which is used to calculate the
337 * number of commits per time-interval. 348 * number of commits per time-interval.
338 */ 349 */
339void cgit_show_stats(struct cgit_context *ctx) 350void cgit_show_stats(struct cgit_context *ctx)
340{ 351{
341 struct string_list authors; 352 struct string_list authors;
342 struct Period *period; 353 struct cgit_period *period;
343 int top, i; 354 int top, i;
355 const char *code = "w";
344 356
345 period = &periods[0]; 357 if (ctx->qry.period)
346 if (ctx->qry.period) { 358 code = ctx->qry.period;
347 for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++) 359
348 if (periods[i].code == ctx->qry.period[0]) { 360 i = cgit_find_stats_period(code, &period);
349 period = &periods[i]; 361 if (!i) {
350 break; 362 cgit_print_error(fmt("Unknown statistics type: %c", code));
351 } 363 return;
364 }
365 if (i > ctx->repo->max_stats) {
366 cgit_print_error(fmt("Statistics type disabled: %s",
367 period->name));
368 return;
352 } 369 }
353 authors = collect_stats(ctx, period); 370 authors = collect_stats(ctx, period);
354 qsort(authors.items, authors.nr, sizeof(struct string_list_item), 371 qsort(authors.items, authors.nr, sizeof(struct string_list_item),
355 cmp_total_commits); 372 cmp_total_commits);
356 373
357 top = ctx->qry.ofs; 374 top = ctx->qry.ofs;
358 if (!top) 375 if (!top)
359 top = 10; 376 top = 10;
360 htmlf("<h2>Commits per author per %s", period->name); 377 htmlf("<h2>Commits per author per %s", period->name);
361 if (ctx->qry.path) { 378 if (ctx->qry.path) {
362 html(" (path '"); 379 html(" (path '");
363 html_txt(ctx->qry.path); 380 html_txt(ctx->qry.path);
364 html("')"); 381 html("')");
365 } 382 }
366 html("</h2>"); 383 html("</h2>");
367 384
368 html("<form method='get' action='.' style='float: right; text-align: right;'>"); 385 html("<form method='get' action='.' style='float: right; text-align: right;'>");
369 if (strcmp(ctx->qry.head, ctx->repo->defbranch)) 386 if (strcmp(ctx->qry.head, ctx->repo->defbranch))
370 htmlf("<input type='hidden' name='h' value='%s'/>", ctx->qry.head); 387 htmlf("<input type='hidden' name='h' value='%s'/>", ctx->qry.head);
371 html("Period: "); 388 if (ctx->repo->max_stats > 1) {
372 html("<select name='period' onchange='this.form.submit();'>"); 389 html("Period: ");
373 for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++) 390 html("<select name='period' onchange='this.form.submit();'>");
374 htmlf("<option value='%c'%s>%s</option>", 391 for (i = 0; i < ctx->repo->max_stats; i++)
375 periods[i].code, 392 htmlf("<option value='%c'%s>%s</option>",
376 period == &periods[i] ? " selected" : "", 393 periods[i].code,
377 periods[i].name); 394 period == &periods[i] ? " selected" : "",
378 html("</select><br/><br/>"); 395 periods[i].name);
396 html("</select><br/><br/>");
397 }
379 html("Authors: "); 398 html("Authors: ");
380 html(""); 399 html("");
381 html("<select name='ofs' onchange='this.form.submit();'>"); 400 html("<select name='ofs' onchange='this.form.submit();'>");
382 htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : ""); 401 htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : "");
383 htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : ""); 402 htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : "");
384 htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : ""); 403 htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : "");
385 htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : ""); 404 htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : "");
386 htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : ""); 405 htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : "");
387 html("</select>"); 406 html("</select>");
388 html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>"); 407 html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>");
389 html("</form>"); 408 html("</form>");
390 print_authors(&authors, top, period); 409 print_authors(&authors, top, period);
391} 410}
392 411
diff --git a/ui-stats.h b/ui-stats.h
index f1d744c..4f13dba 100644
--- a/ui-stats.h
+++ b/ui-stats.h
@@ -1,8 +1,27 @@
1#ifndef UI_STATS_H 1#ifndef UI_STATS_H
2#define UI_STATS_H 2#define UI_STATS_H
3 3
4#include "cgit.h" 4#include "cgit.h"
5 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
6extern void cgit_show_stats(struct cgit_context *ctx); 25extern void cgit_show_stats(struct cgit_context *ctx);
7 26
8#endif /* UI_STATS_H */ 27#endif /* UI_STATS_H */