summaryrefslogtreecommitdiffabout
Side-by-side diff
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,82 +1,83 @@
/* cgit.c: cgi for the git scm
*
* Copyright (C) 2006 Lars Hjemli
*
* Licensed under GNU General Public License v2
* (see COPYING for full license text)
*/
#include "cgit.h"
#include "cache.h"
#include "cmd.h"
#include "configfile.h"
#include "html.h"
#include "ui-shared.h"
+#include "ui-stats.h"
#include "scan-tree.h"
const char *cgit_version = CGIT_VERSION;
void config_cb(const char *name, const char *value)
{
if (!strcmp(name, "root-title"))
ctx.cfg.root_title = xstrdup(value);
else if (!strcmp(name, "root-desc"))
ctx.cfg.root_desc = xstrdup(value);
else if (!strcmp(name, "root-readme"))
ctx.cfg.root_readme = xstrdup(value);
else if (!strcmp(name, "css"))
ctx.cfg.css = xstrdup(value);
else if (!strcmp(name, "favicon"))
ctx.cfg.favicon = xstrdup(value);
else if (!strcmp(name, "footer"))
ctx.cfg.footer = xstrdup(value);
else if (!strcmp(name, "logo"))
ctx.cfg.logo = xstrdup(value);
else if (!strcmp(name, "index-header"))
ctx.cfg.index_header = xstrdup(value);
else if (!strcmp(name, "index-info"))
ctx.cfg.index_info = xstrdup(value);
else if (!strcmp(name, "logo-link"))
ctx.cfg.logo_link = xstrdup(value);
else if (!strcmp(name, "module-link"))
ctx.cfg.module_link = xstrdup(value);
else if (!strcmp(name, "virtual-root")) {
ctx.cfg.virtual_root = trim_end(value, '/');
if (!ctx.cfg.virtual_root && (!strcmp(value, "/")))
ctx.cfg.virtual_root = "";
} else if (!strcmp(name, "nocache"))
ctx.cfg.nocache = atoi(value);
else if (!strcmp(name, "snapshots"))
ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
else if (!strcmp(name, "enable-index-links"))
ctx.cfg.enable_index_links = atoi(value);
else if (!strcmp(name, "enable-log-filecount"))
ctx.cfg.enable_log_filecount = atoi(value);
else if (!strcmp(name, "enable-log-linecount"))
ctx.cfg.enable_log_linecount = atoi(value);
- else if (!strcmp(name, "enable-stats"))
- ctx.cfg.enable_stats = atoi(value);
+ else if (!strcmp(name, "max-stats"))
+ ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
else if (!strcmp(name, "cache-size"))
ctx.cfg.cache_size = atoi(value);
else if (!strcmp(name, "cache-root"))
ctx.cfg.cache_root = xstrdup(value);
else if (!strcmp(name, "cache-root-ttl"))
ctx.cfg.cache_root_ttl = atoi(value);
else if (!strcmp(name, "cache-repo-ttl"))
ctx.cfg.cache_repo_ttl = atoi(value);
else if (!strcmp(name, "cache-static-ttl"))
ctx.cfg.cache_static_ttl = atoi(value);
else if (!strcmp(name, "cache-dynamic-ttl"))
ctx.cfg.cache_dynamic_ttl = atoi(value);
else if (!strcmp(name, "max-message-length"))
ctx.cfg.max_msg_len = atoi(value);
else if (!strcmp(name, "max-repodesc-length"))
ctx.cfg.max_repodesc_len = atoi(value);
else if (!strcmp(name, "max-repo-count"))
ctx.cfg.max_repo_count = atoi(value);
else if (!strcmp(name, "max-commit-count"))
ctx.cfg.max_commit_count = atoi(value);
else if (!strcmp(name, "summary-log"))
ctx.cfg.summary_log = atoi(value);
else if (!strcmp(name, "summary-branches"))
ctx.cfg.summary_branches = atoi(value);
@@ -93,50 +94,50 @@ void config_cb(const char *name, const char *value)
else if (!strcmp(name, "local-time"))
ctx.cfg.local_time = atoi(value);
else if (!strcmp(name, "repo.group"))
ctx.cfg.repo_group = xstrdup(value);
else if (!strcmp(name, "repo.url"))
ctx.repo = cgit_add_repo(value);
else if (!strcmp(name, "repo.name"))
ctx.repo->name = xstrdup(value);
else if (ctx.repo && !strcmp(name, "repo.path"))
ctx.repo->path = trim_end(value, '/');
else if (ctx.repo && !strcmp(name, "repo.clone-url"))
ctx.repo->clone_url = xstrdup(value);
else if (ctx.repo && !strcmp(name, "repo.desc"))
ctx.repo->desc = xstrdup(value);
else if (ctx.repo && !strcmp(name, "repo.owner"))
ctx.repo->owner = xstrdup(value);
else if (ctx.repo && !strcmp(name, "repo.defbranch"))
ctx.repo->defbranch = xstrdup(value);
else if (ctx.repo && !strcmp(name, "repo.snapshots"))
ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */
else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount"))
ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
- else if (ctx.repo && !strcmp(name, "repo.enable-stats"))
- ctx.repo->enable_stats = ctx.cfg.enable_stats && atoi(value);
+ else if (ctx.repo && !strcmp(name, "repo.max-stats"))
+ ctx.repo->max_stats = cgit_find_stats_period(value, NULL);
else if (ctx.repo && !strcmp(name, "repo.module-link"))
ctx.repo->module_link= xstrdup(value);
else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
if (*value == '/')
ctx.repo->readme = xstrdup(value);
else
ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
} else if (!strcmp(name, "include"))
parse_configfile(value, config_cb);
}
static void querystring_cb(const char *name, const char *value)
{
if (!strcmp(name,"r")) {
ctx.qry.repo = xstrdup(value);
ctx.repo = cgit_get_repoinfo(value);
} else if (!strcmp(name, "p")) {
ctx.qry.page = xstrdup(value);
} else if (!strcmp(name, "url")) {
ctx.qry.url = xstrdup(value);
cgit_parse_url(value);
} else if (!strcmp(name, "qt")) {
ctx.qry.grep = xstrdup(value);
} else if (!strcmp(name, "q")) {
@@ -162,48 +163,49 @@ static void querystring_cb(const char *name, const char *value)
ctx.qry.period = xstrdup(value);
}
}
static void prepare_context(struct cgit_context *ctx)
{
memset(ctx, 0, sizeof(ctx));
ctx->cfg.agefile = "info/web/last-modified";
ctx->cfg.nocache = 0;
ctx->cfg.cache_size = 0;
ctx->cfg.cache_dynamic_ttl = 5;
ctx->cfg.cache_max_create_time = 5;
ctx->cfg.cache_repo_ttl = 5;
ctx->cfg.cache_root = CGIT_CACHE_ROOT;
ctx->cfg.cache_root_ttl = 5;
ctx->cfg.cache_static_ttl = -1;
ctx->cfg.css = "/cgit.css";
ctx->cfg.logo = "/git-logo.png";
ctx->cfg.local_time = 0;
ctx->cfg.max_repo_count = 50;
ctx->cfg.max_commit_count = 50;
ctx->cfg.max_lock_attempts = 5;
ctx->cfg.max_msg_len = 80;
ctx->cfg.max_repodesc_len = 80;
+ ctx->cfg.max_stats = 0;
ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s";
ctx->cfg.renamelimit = -1;
ctx->cfg.robots = "index, nofollow";
ctx->cfg.root_title = "Git repository browser";
ctx->cfg.root_desc = "a fast webinterface for the git dscm";
ctx->cfg.script_name = CGIT_SCRIPT_NAME;
ctx->cfg.summary_branches = 10;
ctx->cfg.summary_log = 10;
ctx->cfg.summary_tags = 10;
ctx->page.mimetype = "text/html";
ctx->page.charset = PAGE_ENCODING;
ctx->page.filename = NULL;
ctx->page.size = 0;
ctx->page.modified = time(NULL);
ctx->page.expires = ctx->page.modified;
}
struct refmatch {
char *req_ref;
char *first_ref;
int match;
};
int find_current_ref(const char *refname, const unsigned char *sha1,
diff --git a/cgit.h b/cgit.h
index 85045c4..f2cb671 100644
--- a/cgit.h
+++ b/cgit.h
@@ -40,49 +40,49 @@
/*
* Default encoding
*/
#define PAGE_ENCODING "UTF-8"
typedef void (*configfn)(const char *name, const char *value);
typedef void (*filepair_fn)(struct diff_filepair *pair);
typedef void (*linediff_fn)(char *line, int len);
struct cgit_repo {
char *url;
char *name;
char *path;
char *desc;
char *owner;
char *defbranch;
char *group;
char *module_link;
char *readme;
char *clone_url;
int snapshots;
int enable_log_filecount;
int enable_log_linecount;
- int enable_stats;
+ int max_stats;
};
struct cgit_repolist {
int length;
int count;
struct cgit_repo *repos;
};
struct commitinfo {
struct commit *commit;
char *author;
char *author_email;
unsigned long author_date;
char *committer;
char *committer_email;
unsigned long committer_date;
char *subject;
char *msg;
char *msg_encoding;
};
struct taginfo {
char *tagger;
char *tagger_email;
@@ -132,55 +132,55 @@ struct cgit_config {
char *css;
char *favicon;
char *footer;
char *index_header;
char *index_info;
char *logo;
char *logo_link;
char *module_link;
char *repo_group;
char *robots;
char *root_title;
char *root_desc;
char *root_readme;
char *script_name;
char *virtual_root;
int cache_size;
int cache_dynamic_ttl;
int cache_max_create_time;
int cache_repo_ttl;
int cache_root_ttl;
int cache_static_ttl;
int enable_index_links;
int enable_log_filecount;
int enable_log_linecount;
- int enable_stats;
int local_time;
int max_repo_count;
int max_commit_count;
int max_lock_attempts;
int max_msg_len;
int max_repodesc_len;
+ int max_stats;
int nocache;
int renamelimit;
int snapshots;
int summary_branches;
int summary_log;
int summary_tags;
};
struct cgit_page {
time_t modified;
time_t expires;
size_t size;
char *mimetype;
char *charset;
char *filename;
char *title;
};
struct cgit_context {
struct cgit_query qry;
struct cgit_config cfg;
struct cgit_repo *repo;
struct cgit_page page;
};
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 60d3ea4..0bbbea3 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -53,52 +53,48 @@ clone-prefix
Space-separated list of common prefixes which, when combined with a
repository url, generates valid clone urls for the repository. This
setting is only used if `repo.clone-url` is unspecified. Default value:
none.
css
Url which specifies the css document to include in all cgit pages.
Default value: "/cgit.css".
enable-index-links
Flag which, when set to "1", will make cgit generate extra links for
each repo in the repository index (specifically, to the "summary",
"commit" and "tree" pages). Default value: "0".
enable-log-filecount
Flag which, when set to "1", will make cgit print the number of
modified files for each commit on the repository log page. Default
value: "0".
enable-log-linecount
Flag which, when set to "1", will make cgit print the number of added
and removed lines for each commit on the repository log page. Default
value: "0".
-enable-stats
- Globally enable/disable statistics for each repository. Default
- value: "0".
-
favicon
Url used as link to a shortcut icon for cgit. If specified, it is
suggested to use the value "/favicon.ico" since certain browsers will
ignore other values. Default value: none.
footer
The content of the file specified with this option will be included
verbatim at the bottom of all pages (i.e. it replaces the standard
"generated by..." message. Default value: none.
include
Name of a configfile to include before the rest of the current config-
file is parsed. Default value: none.
index-header
The content of the file specified with this option will be included
verbatim above the repository index. This setting is deprecated, and
will not be supported by cgit-1.0 (use root-readme instead). Default
value: none.
index-info
The content of the file specified with this option will be included
verbatim below the heading on the repository index page. This setting
is deprecated, and will not be supported by cgit-1.0 (use root-desc
@@ -112,48 +108,53 @@ logo
Url which specifies the source of an image which will be used as a logo
on all cgit pages.
logo-link
Url loaded when clicking on the cgit logo image. If unspecified the
calculated url of the repository index page will be used. Default
value: none.
max-commit-count
Specifies the number of entries to list per page in "log" view. Default
value: "50".
max-message-length
Specifies the maximum number of commit message characters to display in
"log" view. Default value: "80".
max-repo-count
Specifies the number of entries to list per page on the repository
index page. Default value: "50".
max-repodesc-length
Specifies the maximum number of repo description characters to display
on the repository index page. Default value: "80".
+max-stats
+ Set the default maximum statistics period. Valid values are "week",
+ "month", "quarter" and "year". If unspecified, statistics are
+ disabled. Default value: none. See also: "repo.max-stats".
+
module-link
Text which will be used as the formatstring for a hyperlink when a
submodule is printed in a directory listing. The arguments for the
formatstring are the path and SHA1 of the submodule commit. Default
value: "./?repo=%s&page=commit&id=%s"
nocache
If set to the value "1" caching will be disabled. This settings is
deprecated, and will not be honored starting with cgit-1.0. Default
value: "0".
renamelimit
Maximum number of files to consider when detecting renames. The value
"-1" uses the compiletime value in git (for further info, look at
`man git-diff`). Default value: "-1".
repo.group
A value for the current repository group, which all repositories
specified after this setting will inherit. Default value: none.
robots
Text used as content for the "robots" meta-tag. Default value:
"index, nofollow".
@@ -201,51 +202,52 @@ virtual-root
same kind of virtual urls, so this option will probably be deprecated.
REPOSITORY SETTINGS
-------------------
repo.clone-url
A list of space-separated urls which can be used to clone this repo.
Default value: none.
repo.defbranch
The name of the default branch for this repository. If no such branch
exists in the repository, the first branch name (when sorted) is used
as default instead. Default value: "master".
repo.desc
The value to show as repository description. Default value: none.
repo.enable-log-filecount
A flag which can be used to disable the global setting
`enable-log-filecount'. Default value: none.
repo.enable-log-linecount
A flag which can be used to disable the global setting
`enable-log-linecount'. Default value: none.
-repo.enable-stats
- A flag which can be used to disable the global setting
- `enable-stats'. Default value: none.
+repo.max-stats
+ Override the default maximum statistics period. Valid values are equal
+ to the values specified for the global "max-stats" setting. Default
+ value: none.
repo.name
The value to show as repository name. Default value: <repo.url>.
repo.owner
A value used to identify the owner of the repository. Default value:
none.
repo.path
An absolute path to the repository directory. For non-bare repositories
this is the .git-directory. Default value: none.
repo.readme
A path (relative to <repo.path>) which specifies a file to include
verbatim as the "About" page for this repo. Default value: none.
repo.snapshots
A mask of allowed snapshot-formats for this repo, restricted by the
"snapshots" global setting. Default value: <snapshots>.
repo.url
The relative url used to access the repository. This must be the first
setting specified for each repo. Default value: none.
@@ -263,48 +265,52 @@ clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git
# Specify the css url
css=/css/cgit.css
# Show extra links for each repository on the index page
enable-index-links=1
# Show number of affected files per commit on the log pages
enable-log-filecount=1
# Show number of added/removed lines per commit on the log pages
enable-log-linecount=1
# Add a cgit favicon
favicon=/favicon.ico
# Use a custom logo
logo=/img/mylogo.png
+# Enable statistics per week, month and quarter
+max-stats=quarter
+
+
# Set the title and heading of the repository index page
root-title=foobar.com git repositories
# Set a subheading for the repository index page
root-desc=tracking the foobar development
# Include some more info about foobar.com on the index page
root-readme=/var/www/htdocs/about.html
# Allow download of tar.gz, tar.bz and zip-files
snapshots=tar.gz tar.bz zip
##
## List of repositories.
## PS: Any repositories listed when repo.group is unset will not be
## displayed under a group heading
## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos')
## and included like this:
## include=/etc/cgitrepos
##
@@ -335,41 +341,44 @@ repo.desc=a set of extensions for bar users
repo.url=wiz
repo.path=/pub/git/wiz.git
repo.desc=the wizard of foo
# Add some mirrored repositories
repo.group=mirrors
repo.url=git
repo.path=/pub/git/git.git
repo.desc=the dscm
repo.url=linux
repo.path=/pub/git/linux.git
repo.desc=the kernel
# Disable adhoc downloads of this repo
repo.snapshots=0
# Disable line-counts for this repo
repo.enable-log-linecount=0
+# Restrict the max statistics period for this repo
+repo.max-stats=month
+
BUGS
----
Comments currently cannot appear on the same line as a setting; the comment
will be included as part of the value. E.g. this line:
robots=index # allow indexing
will generate the following html element:
<meta name='robots' content='index # allow indexing'/>
AUTHOR
------
Lars Hjemli <hjemli@gmail.com>
diff --git a/cmd.c b/cmd.c
index 744bf84..763a558 100644
--- a/cmd.c
+++ b/cmd.c
@@ -91,52 +91,49 @@ static void repolist_fn(struct cgit_context *ctx)
static void patch_fn(struct cgit_context *ctx)
{
cgit_print_patch(ctx->qry.sha1);
}
static void plain_fn(struct cgit_context *ctx)
{
cgit_print_plain(ctx);
}
static void refs_fn(struct cgit_context *ctx)
{
cgit_print_refs();
}
static void snapshot_fn(struct cgit_context *ctx)
{
cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1,
cgit_repobasename(ctx->repo->url), ctx->qry.path,
ctx->repo->snapshots, ctx->qry.nohead);
}
static void stats_fn(struct cgit_context *ctx)
{
- if (ctx->repo->enable_stats)
- cgit_show_stats(ctx);
- else
- cgit_print_error("Stats disabled for this repo");
+ cgit_show_stats(ctx);
}
static void summary_fn(struct cgit_context *ctx)
{
cgit_print_summary();
}
static void tag_fn(struct cgit_context *ctx)
{
cgit_print_tag(ctx->qry.sha1);
}
static void tree_fn(struct cgit_context *ctx)
{
cgit_print_tree(ctx->qry.sha1, ctx->qry.path);
}
#define def_cmd(name, want_repo, want_layout) \
{#name, name##_fn, want_repo, want_layout}
struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx)
{
static struct cgit_cmd cmds[] = {
def_cmd(HEAD, 1, 0),
diff --git a/shared.c b/shared.c
index 37333f0..7382609 100644
--- a/shared.c
+++ b/shared.c
@@ -37,49 +37,49 @@ struct cgit_repo *cgit_add_repo(const char *url)
{
struct cgit_repo *ret;
if (++cgit_repolist.count > cgit_repolist.length) {
if (cgit_repolist.length == 0)
cgit_repolist.length = 8;
else
cgit_repolist.length *= 2;
cgit_repolist.repos = xrealloc(cgit_repolist.repos,
cgit_repolist.length *
sizeof(struct cgit_repo));
}
ret = &cgit_repolist.repos[cgit_repolist.count-1];
ret->url = trim_end(url, '/');
ret->name = ret->url;
ret->path = NULL;
ret->desc = "[no description]";
ret->owner = NULL;
ret->group = ctx.cfg.repo_group;
ret->defbranch = "master";
ret->snapshots = ctx.cfg.snapshots;
ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
- ret->enable_stats = ctx.cfg.enable_stats;
+ ret->max_stats = ctx.cfg.max_stats;
ret->module_link = ctx.cfg.module_link;
ret->readme = NULL;
return ret;
}
struct cgit_repo *cgit_get_repoinfo(const char *url)
{
int i;
struct cgit_repo *repo;
for (i=0; i<cgit_repolist.count; i++) {
repo = &cgit_repolist.repos[i];
if (!strcmp(repo->url, url))
return repo;
}
return NULL;
}
void *cgit_free_commitinfo(struct commitinfo *info)
{
free(info->author);
free(info->author_email);
free(info->committer);
free(info->committer_email);
diff --git a/ui-shared.c b/ui-shared.c
index 0e688a0..97b9d46 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -620,49 +620,49 @@ void cgit_print_pageheader(struct cgit_context *ctx)
html("</td><td class='sub right'>");
html_txt(ctx->repo->owner);
} else {
if (ctx->cfg.root_desc)
html_txt(ctx->cfg.root_desc);
else if (ctx->cfg.index_info)
html_include(ctx->cfg.index_info);
}
html("</td></tr></table>\n");
html("<table class='tabs'><tr><td>\n");
if (ctx->repo) {
cgit_summary_link("summary", NULL, hc(cmd, "summary"),
ctx->qry.head);
cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head,
ctx->qry.sha1, NULL);
cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head,
NULL, NULL, 0, NULL, NULL);
cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
ctx->qry.sha1, NULL);
cgit_commit_link("commit", NULL, hc(cmd, "commit"),
ctx->qry.head, ctx->qry.sha1);
cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
ctx->qry.sha1, ctx->qry.sha2, NULL);
- if (ctx->repo->enable_stats)
+ if (ctx->repo->max_stats)
reporevlink("stats", "stats", NULL, hc(cmd, "stats"),
ctx->qry.head, NULL, NULL);
if (ctx->repo->readme)
reporevlink("about", "about", NULL,
hc(cmd, "about"), ctx->qry.head, NULL,
NULL);
html("</td><td class='form'>");
html("<form class='right' method='get' action='");
if (ctx->cfg.virtual_root)
html_url_path(cgit_fileurl(ctx->qry.repo, "log",
ctx->qry.path, NULL));
html("'>\n");
add_hidden_formfields(1, 0, "log");
html("<select name='qt'>\n");
html_option("grep", "log msg", ctx->qry.grep);
html_option("author", "author", ctx->qry.grep);
html_option("committer", "committer", ctx->qry.grep);
html("</select>\n");
html("<input class='txt' type='text' size='10' name='q' value='");
html_attr(ctx->qry.search);
html("'/>\n");
html("<input type='submit' value='search'/>\n");
html("</form>\n");
} else {
diff --git a/ui-stats.c b/ui-stats.c
index 3cc8d70..1104485 100644
--- a/ui-stats.c
+++ b/ui-stats.c
@@ -1,47 +1,33 @@
+#include <string-list.h>
+
#include "cgit.h"
#include "html.h"
-#include <string-list.h>
+#include "ui-shared.h"
+#include "ui-stats.h"
#define MONTHS 6
-struct Period {
- const char code;
- const char *name;
- int max_periods;
- int count;
-
- /* Convert a tm value to the first day in the period */
- void (*trunc)(struct tm *tm);
-
- /* Update tm value to start of next/previous period */
- void (*dec)(struct tm *tm);
- void (*inc)(struct tm *tm);
-
- /* Pretty-print a tm value */
- char *(*pretty)(struct tm *tm);
-};
-
struct authorstat {
long total;
struct string_list list;
};
#define DAY_SECS (60 * 60 * 24)
#define WEEK_SECS (DAY_SECS * 7)
static void trunc_week(struct tm *tm)
{
time_t t = timegm(tm);
t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
gmtime_r(&t, tm);
}
static void dec_week(struct tm *tm)
{
time_t t = timegm(tm);
t -= WEEK_SECS;
gmtime_r(&t, tm);
}
static void inc_week(struct tm *tm)
{
@@ -116,183 +102,208 @@ static char *pretty_quarter(struct tm *tm)
return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
}
static void trunc_year(struct tm *tm)
{
trunc_month(tm);
tm->tm_mon = 0;
}
static void dec_year(struct tm *tm)
{
tm->tm_year--;
}
static void inc_year(struct tm *tm)
{
tm->tm_year++;
}
static char *pretty_year(struct tm *tm)
{
return fmt("%d", tm->tm_year + 1900);
}
-struct Period periods[] = {
+struct cgit_period periods[] = {
{'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
{'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
{'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
{'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
};
+/* Given a period code or name, return a period index (1, 2, 3 or 4)
+ * and update the period pointer to the correcsponding struct.
+ * If no matching code is found, return 0.
+ */
+int cgit_find_stats_period(const char *expr, struct cgit_period **period)
+{
+ int i;
+ char code = '\0';
+
+ if (!expr)
+ return 0;
+
+ if (strlen(expr) == 1)
+ code = expr[0];
+
+ for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
+ if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
+ if (period)
+ *period = &periods[i];
+ return i+1;
+ }
+ return 0;
+}
+
static void add_commit(struct string_list *authors, struct commit *commit,
- struct Period *period)
+ struct cgit_period *period)
{
struct commitinfo *info;
struct string_list_item *author, *item;
struct authorstat *authorstat;
struct string_list *items;
char *tmp;
struct tm *date;
time_t t;
info = cgit_parse_commit(commit);
tmp = xstrdup(info->author);
author = string_list_insert(tmp, authors);
if (!author->util)
author->util = xcalloc(1, sizeof(struct authorstat));
else
free(tmp);
authorstat = author->util;
items = &authorstat->list;
t = info->committer_date;
date = gmtime(&t);
period->trunc(date);
tmp = xstrdup(period->pretty(date));
item = string_list_insert(tmp, items);
if (item->util)
free(tmp);
item->util++;
authorstat->total++;
cgit_free_commitinfo(info);
}
static int cmp_total_commits(const void *a1, const void *a2)
{
const struct string_list_item *i1 = a1;
const struct string_list_item *i2 = a2;
const struct authorstat *auth1 = i1->util;
const struct authorstat *auth2 = i2->util;
return auth2->total - auth1->total;
}
/* Walk the commit DAG and collect number of commits per author per
* timeperiod into a nested string_list collection.
*/
struct string_list collect_stats(struct cgit_context *ctx,
- struct Period *period)
+ struct cgit_period *period)
{
struct string_list authors;
struct rev_info rev;
struct commit *commit;
const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL};
int argc = 3;
time_t now;
long i;
struct tm *tm;
char tmp[11];
time(&now);
tm = gmtime(&now);
period->trunc(tm);
for (i = 1; i < period->count; i++)
period->dec(tm);
strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
argv[2] = xstrdup(fmt("--since=%s", tmp));
if (ctx->qry.path) {
argv[3] = "--";
argv[4] = ctx->qry.path;
argc += 2;
}
init_revisions(&rev, NULL);
rev.abbrev = DEFAULT_ABBREV;
rev.commit_format = CMIT_FMT_DEFAULT;
rev.no_merges = 1;
rev.verbose_header = 1;
rev.show_root_diff = 0;
setup_revisions(argc, argv, &rev, NULL);
prepare_revision_walk(&rev);
memset(&authors, 0, sizeof(authors));
while ((commit = get_revision(&rev)) != NULL) {
add_commit(&authors, commit, period);
free(commit->buffer);
free_commit_list(commit->parents);
}
return authors;
}
void print_combined_authorrow(struct string_list *authors, int from, int to,
const char *name, const char *leftclass, const char *centerclass,
- const char *rightclass, struct Period *period)
+ const char *rightclass, struct cgit_period *period)
{
struct string_list_item *author;
struct authorstat *authorstat;
struct string_list *items;
struct string_list_item *date;
time_t now;
long i, j, total, subtotal;
struct tm *tm;
char *tmp;
time(&now);
tm = gmtime(&now);
period->trunc(tm);
for (i = 1; i < period->count; i++)
period->dec(tm);
total = 0;
htmlf("<tr><td class='%s'>%s</td>", leftclass,
fmt(name, to - from + 1));
for (j = 0; j < period->count; j++) {
tmp = period->pretty(tm);
period->inc(tm);
subtotal = 0;
for (i = from; i <= to; i++) {
author = &authors->items[i];
authorstat = author->util;
items = &authorstat->list;
date = string_list_lookup(tmp, items);
if (date)
subtotal += (size_t)date->util;
}
htmlf("<td class='%s'>%d</td>", centerclass, subtotal);
total += subtotal;
}
htmlf("<td class='%s'>%d</td></tr>", rightclass, total);
}
-void print_authors(struct string_list *authors, int top, struct Period *period)
+void print_authors(struct string_list *authors, int top,
+ struct cgit_period *period)
{
struct string_list_item *author;
struct authorstat *authorstat;
struct string_list *items;
struct string_list_item *date;
time_t now;
long i, j, total;
struct tm *tm;
char *tmp;
time(&now);
tm = gmtime(&now);
period->trunc(tm);
for (i = 1; i < period->count; i++)
period->dec(tm);
html("<table class='stats'><tr><th>Author</th>");
for (j = 0; j < period->count; j++) {
tmp = period->pretty(tm);
htmlf("<th>%s</th>", tmp);
period->inc(tm);
}
html("<th>Total</th></tr>\n");
@@ -318,75 +329,83 @@ void print_authors(struct string_list *authors, int top, struct Period *period)
else {
htmlf("<td>%d</td>", date->util);
total += (size_t)date->util;
}
}
htmlf("<td class='sum'>%d</td></tr>", total);
}
if (top < authors->nr)
print_combined_authorrow(authors, top, authors->nr - 1,
"Others (%d)", "left", "", "sum", period);
print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
"total", "sum", "sum", period);
html("</table>");
}
/* Create a sorted string_list with one entry per author. The util-field
* for each author is another string_list which is used to calculate the
* number of commits per time-interval.
*/
void cgit_show_stats(struct cgit_context *ctx)
{
struct string_list authors;
- struct Period *period;
+ struct cgit_period *period;
int top, i;
+ const char *code = "w";
- period = &periods[0];
- if (ctx->qry.period) {
- for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
- if (periods[i].code == ctx->qry.period[0]) {
- period = &periods[i];
- break;
- }
+ if (ctx->qry.period)
+ code = ctx->qry.period;
+
+ i = cgit_find_stats_period(code, &period);
+ if (!i) {
+ cgit_print_error(fmt("Unknown statistics type: %c", code));
+ return;
+ }
+ if (i > ctx->repo->max_stats) {
+ cgit_print_error(fmt("Statistics type disabled: %s",
+ period->name));
+ return;
}
authors = collect_stats(ctx, period);
qsort(authors.items, authors.nr, sizeof(struct string_list_item),
cmp_total_commits);
top = ctx->qry.ofs;
if (!top)
top = 10;
htmlf("<h2>Commits per author per %s", period->name);
if (ctx->qry.path) {
html(" (path '");
html_txt(ctx->qry.path);
html("')");
}
html("</h2>");
html("<form method='get' action='.' style='float: right; text-align: right;'>");
if (strcmp(ctx->qry.head, ctx->repo->defbranch))
htmlf("<input type='hidden' name='h' value='%s'/>", ctx->qry.head);
- html("Period: ");
- html("<select name='period' onchange='this.form.submit();'>");
- for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
- htmlf("<option value='%c'%s>%s</option>",
- periods[i].code,
- period == &periods[i] ? " selected" : "",
- periods[i].name);
- html("</select><br/><br/>");
+ if (ctx->repo->max_stats > 1) {
+ html("Period: ");
+ html("<select name='period' onchange='this.form.submit();'>");
+ for (i = 0; i < ctx->repo->max_stats; i++)
+ htmlf("<option value='%c'%s>%s</option>",
+ periods[i].code,
+ period == &periods[i] ? " selected" : "",
+ periods[i].name);
+ html("</select><br/><br/>");
+ }
html("Authors: ");
html("");
html("<select name='ofs' onchange='this.form.submit();'>");
htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : "");
htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : "");
htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : "");
htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : "");
htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : "");
html("</select>");
html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>");
html("</form>");
print_authors(&authors, top, period);
}
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 @@
#ifndef UI_STATS_H
#define UI_STATS_H
#include "cgit.h"
+struct cgit_period {
+ const char code;
+ const char *name;
+ int max_periods;
+ int count;
+
+ /* Convert a tm value to the first day in the period */
+ void (*trunc)(struct tm *tm);
+
+ /* Update tm value to start of next/previous period */
+ void (*dec)(struct tm *tm);
+ void (*inc)(struct tm *tm);
+
+ /* Pretty-print a tm value */
+ char *(*pretty)(struct tm *tm);
+};
+
+extern int cgit_find_stats_period(const char *expr, struct cgit_period **period);
+
extern void cgit_show_stats(struct cgit_context *ctx);
#endif /* UI_STATS_H */