]> gitweb.ps.run Git - ps-cgit/commitdiff
Merge branch 'lh/stats'
authorLars Hjemli <hjemli@gmail.com>
Tue, 27 Jan 2009 19:16:37 +0000 (20:16 +0100)
committerLars Hjemli <hjemli@gmail.com>
Tue, 27 Jan 2009 19:16:37 +0000 (20:16 +0100)
Conflicts:
cgit.c
cgit.css
cgit.h
ui-tree.c

Signed-off-by: Lars Hjemli <hjemli@gmail.com>
1  2 
Makefile
cgit.c
cgit.css
cgit.h
cgitrc.5.txt
cmd.c
shared.c
ui-shared.c
ui-shared.h
ui-tree.c

diff --combined Makefile
index 7793c0b571ee4aa1e07b92593b7d6f95fb3c341f,f426f985982f20dff008c4254e23ebf0afc17a98..a52285e0ef5809284b724afbe2051e368328ecf6
+++ b/Makefile
@@@ -1,33 -1,11 +1,33 @@@
  CGIT_VERSION = v0.8.1
  CGIT_SCRIPT_NAME = cgit.cgi
  CGIT_SCRIPT_PATH = /var/www/htdocs/cgit
 +CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH)
  CGIT_CONFIG = /etc/cgitrc
  CACHE_ROOT = /var/cache/cgit
  SHA1_HEADER = <openssl/sha.h>
 -GIT_VER = 1.6.0.2
 +GIT_VER = 1.6.1
  GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2
 +INSTALL = install
 +
 +# Define NO_STRCASESTR if you don't have strcasestr.
 +#
 +# Define NEEDS_LIBICONV if linking with libc is not enough (eg. Darwin).
 +#
 +
 +#-include config.mak
 +
 +#
 +# Platform specific tweaks
 +#
 +
 +uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
 +uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not')
 +uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not')
 +
 +ifeq ($(uname_O),Cygwin)
 +      NO_STRCASESTR = YesPlease
 +      NEEDS_LIBICONV = YesPlease
 +endif
  
  #
  # Let the user override the above settings.
@@@ -90,6 -68,7 +90,7 @@@ OBJECTS += ui-refs.
  OBJECTS += ui-repolist.o
  OBJECTS += ui-shared.o
  OBJECTS += ui-snapshot.o
+ OBJECTS += ui-stats.o
  OBJECTS += ui-summary.o
  OBJECTS += ui-tag.o
  OBJECTS += ui-tree.o
@@@ -118,9 -97,6 +119,9 @@@ CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_R
  ifdef NO_ICONV
        CFLAGS += -DNO_ICONV
  endif
 +ifdef NO_STRCASESTR
 +      CFLAGS += -DNO_STRCASESTR
 +endif
  
  cgit: $(OBJECTS) libgit
        $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS)
@@@ -130,23 -106,22 +131,23 @@@ cgit.o: VERSIO
  -include $(OBJECTS:.o=.d)
  
  libgit:
 -      $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) libgit.a
 -      $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) xdiff/lib.a
 +      $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 libgit.a
 +      $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 xdiff/lib.a
  
  test: all
        $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all
  
  install: all
 -      mkdir -p $(DESTDIR)$(CGIT_SCRIPT_PATH)
 -      install cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
 -      install cgit.css $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.css
 -      install cgit.png $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.png
 +      $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH)
 +      $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
 +      $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH)
 +      $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css
 +      $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png
  
  uninstall:
        rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
 -      rm -f $(CGIT_SCRIPT_PATH)/cgit.css
 -      rm -f $(CGIT_SCRIPT_PATH)/cgit.png
 +      rm -f $(CGIT_DATA_PATH)/cgit.css
 +      rm -f $(CGIT_DATA_PATH)/cgit.png
  
  clean:
        rm -f cgit VERSION *.o *.d
diff --combined cgit.c
index f35f6059e1531ad6408b3ec3d638be81643bed58,57e11cdfb0b1095c2bd9b742a12f79793e4841a0..608cab658425e5c11270ac3d1690a842eec71891
--- 1/cgit.c
--- 2/cgit.c
+++ b/cgit.c
@@@ -12,6 -12,7 +12,7 @@@
  #include "configfile.h"
  #include "html.h"
  #include "ui-shared.h"
+ #include "ui-stats.h"
  #include "scan-tree.h"
  
  const char *cgit_version = CGIT_VERSION;
@@@ -54,6 -55,8 +55,8 @@@ void config_cb(const char *name, const 
                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, "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.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.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) {
@@@ -154,10 -159,8 +159,12 @@@ static void querystring_cb(const char *
                ctx.qry.name = xstrdup(value);
        } else if (!strcmp(name, "mimetype")) {
                ctx.qry.mimetype = xstrdup(value);
 +      } else if (!strcmp(name, "s")){
 +              ctx.qry.sort = xstrdup(value);
 +      } else if (!strcmp(name, "showmsg")) {
 +              ctx.qry.showmsg = atoi(value);
+       } else if (!strcmp(name, "period")) {
+               ctx.qry.period = xstrdup(value);
        }
  }
  
@@@ -181,6 -184,7 +188,7 @@@ static void prepare_context(struct cgit
        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";
@@@ -293,6 -297,7 +301,6 @@@ static void process_request(void *cbdat
        cmd = cgit_get_cmd(ctx);
        if (!cmd) {
                ctx->page.title = "cgit error";
 -              ctx->repo = NULL;
                cgit_print_http_headers(ctx);
                cgit_print_docstart(ctx);
                cgit_print_pageheader(ctx);
@@@ -438,29 -443,28 +446,29 @@@ int main(int argc, const char **argv
        ctx.repo = NULL;
        http_parse_querystring(ctx.qry.raw, querystring_cb);
  
 -      /* If virtual-root isn't specified in cgitrc and no url
 -       * parameter is specified on the querystring, lets pretend
 -       * that virtualroot equals SCRIPT_NAME and use PATH_INFO as
 -       * url. This allows cgit to work with virtual urls without
 -       * the need for rewriterules in the webserver (as long as
 -       * PATH_INFO is included in the cache lookup key).
 +      /* If virtual-root isn't specified in cgitrc, lets pretend
 +       * that virtual-root equals SCRIPT_NAME.
         */
 -      if (!ctx.cfg.virtual_root && !ctx.qry.url) {
 +      if (!ctx.cfg.virtual_root)
                ctx.cfg.virtual_root = ctx.cfg.script_name;
 -              path = getenv("PATH_INFO");
 -              if (path) {
 -                      if (path[0] == '/')
 -                              path++;
 -                      ctx.qry.url = xstrdup(path);
 -                      if (ctx.qry.raw) {
 -                              qry = ctx.qry.raw;
 -                              ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
 -                              free(qry);
 -                      } else
 -                              ctx.qry.raw = ctx.qry.url;
 -                      cgit_parse_url(ctx.qry.url);
 -              }
 +
 +      /* If no url parameter is specified on the querystring, lets
 +       * use PATH_INFO as url. This allows cgit to work with virtual
 +       * urls without the need for rewriterules in the webserver (as
 +       * long as PATH_INFO is included in the cache lookup key).
 +       */
 +      path = getenv("PATH_INFO");
 +      if (!ctx.qry.url && path) {
 +              if (path[0] == '/')
 +                      path++;
 +              ctx.qry.url = xstrdup(path);
 +              if (ctx.qry.raw) {
 +                      qry = ctx.qry.raw;
 +                      ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
 +                      free(qry);
 +              } else
 +                      ctx.qry.raw = ctx.qry.url;
 +              cgit_parse_url(ctx.qry.url);
        }
  
        ttl = calc_ttl();
diff --combined cgit.css
index f19446dfbccfffd727b97793fc84dda1115805f9,ef30fbf899803c2a102875c1dfb3d25dd4f55760..e8214de13550e0e18bd2ef1b41f9d375bddb416c
+++ b/cgit.css
@@@ -120,10 -120,6 +120,10 @@@ table.list tr 
        background: white;
  }
  
 +table.list tr.logheader {
 +      background: #eee;
 +}
 +
  table.list tr:hover {
        background: #eee;
  }
@@@ -147,17 -143,6 +147,17 @@@ table.list td 
        padding: 0.1em 0.5em 0.1em 0.5em;
  }
  
 +table.list td.logsubject {
 +      font-family: monospace;
 +      font-weight: bold;
 +}
 +
 +table.list td.logmsg {
 +      font-family: monospace;
 +      white-space: pre;
 +      padding: 1em 0em 2em 0em;
 +}
 +
  table.list td a {
        color: black;
  }
@@@ -471,27 -456,80 +471,103 @@@ div.footer 
        font-size: 80%;
        color: #ccc;
  }
 -
 +a.branch-deco {
 +      margin: 0px 0.5em;
 +      padding: 0px 0.25em;
 +      background-color: #88ff88;
 +      border: solid 1px #007700;
 +}
 +a.tag-deco {
 +      margin: 0px 0.5em;
 +      padding: 0px 0.25em;
 +      background-color: #ffff88;
 +      border: solid 1px #777700;
 +}
 +a.remote-deco {
 +      margin: 0px 0.5em;
 +      padding: 0px 0.25em;
 +      background-color: #ccccff;
 +      border: solid 1px #000077;
 +}
 +a.deco {
 +      margin: 0px 0.5em;
 +      padding: 0px 0.25em;
 +      background-color: #ff8888;
 +      border: solid 1px #770000;
 +}
+ table.stats {
+       border: solid 1px black;
+       border-collapse: collapse;
+ }
+ table.stats th {
+       text-align: left;
+       padding: 1px 0.5em;
+       background-color: #eee;
+       border: solid 1px black;
+ }
+ table.stats td {
+       text-align: right;
+       padding: 1px 0.5em;
+       border: solid 1px black;
+ }
+ table.stats td.total {
+       font-weight: bold;
+       text-align: left;
+ }
+ table.stats td.sum {
+       color: #c00;
+       font-weight: bold;
+ /*    background-color: #eee; */
+ }
+ table.stats td.left {
+       text-align: left;
+ }
+ table.vgraph {
+       border-collapse: separate;
+       border: solid 1px black;
+       height: 200px;
+ }
+ table.vgraph th {
+       background-color: #eee;
+       font-weight: bold;
+       border: solid 1px white;
+       padding: 1px 0.5em;
+ }
+ table.vgraph td {
+       vertical-align: bottom;
+       padding: 0px 10px;
+ }
+ table.vgraph div.bar {
+       background-color: #eee;
+ }
+ table.hgraph {
+       border: solid 1px black;
+       width: 800px;
+ }
+ table.hgraph th {
+       background-color: #eee;
+       font-weight: bold;
+       border: solid 1px black;
+       padding: 1px 0.5em;
+ }
+ table.hgraph td {
+       vertical-align: center;
+       padding: 2px 2px;
+ }
+ table.hgraph div.bar {
+       background-color: #eee;
+       height: 1em;
+ }
diff --combined cgit.h
index cb2f176f621f52d60489dd817d2776411360b3bf,f2cb671b12962d9b13ced83aa2685b4bc343796a..4fe94c69e6939d969572a6c1b4601d5267f390c2
--- 1/cgit.h
--- 2/cgit.h
+++ b/cgit.h
@@@ -61,7 -61,7 +61,8 @@@ struct cgit_repo 
        int snapshots;
        int enable_log_filecount;
        int enable_log_linecount;
+       int max_stats;
 +      time_t mtime;
  };
  
  struct cgit_repolist {
@@@ -120,10 -120,9 +121,11 @@@ struct cgit_query 
        char *name;
        char *mimetype;
        char *url;
+       char *period;
        int   ofs;
        int nohead;
 +      char *sort;
 +      int showmsg;
  };
  
  struct cgit_config {
        int max_lock_attempts;
        int max_msg_len;
        int max_repodesc_len;
+       int max_stats;
        int nocache;
        int renamelimit;
        int snapshots;
@@@ -236,5 -236,11 +239,5 @@@ extern const char *cgit_repobasename(co
  
  extern int cgit_parse_snapshots_mask(const char *str);
  
 -/* libgit.a either links against or compiles its own implementation of
 - * strcasestr(), and we'd like to reuse it. Simply re-declaring it
 - * seems to do the trick.
 - */
 -extern char *strcasestr(const char *haystack, const char *needle);
 -
  
  #endif /* CGIT_H */
diff --combined cgitrc.5.txt
index ab9ab66571b4934f6837186d99cd5712f65a478f,0bbbea3b69c891068bda9e29666877ae2008a119..09f56a68366f015818a18d0f1ec92202d509582a
@@@ -129,6 -129,11 +129,11 @@@ max-repodesc-lengt
        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
@@@ -218,6 -223,11 +223,11 @@@ repo.enable-log-linecoun
        A flag which can be used to disable the global setting
        `enable-log-linecount'. 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>.
  
@@@ -276,6 -286,10 +286,10 @@@ favicon=/favicon.ic
  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
  
@@@ -288,8 -302,8 +302,8 @@@ root-desc=tracking the foobar developme
  root-readme=/var/www/htdocs/about.html
  
  
 -# Allow download of tar.gz, tar.bz and zip-files
 -snapshots=tar.gz tar.bz zip
 +# Allow download of tar.gz, tar.bz2 and zip-files
 +snapshots=tar.gz tar.bz2 zip
  
  
  ##
@@@ -348,6 -362,9 +362,9 @@@ repo.snapshots=
  # Disable line-counts for this repo
  repo.enable-log-linecount=0
  
+ # Restrict the max statistics period for this repo
+ repo.max-stats=month
  
  BUGS
  ----
diff --combined cmd.c
index 8914fa50132b7e87ffc6caa9bfd58a93eedff02c,763a558710a6a4f76590dabcf3a800b3c79aa356..cf97da75a17f733b0627066921dbabf424165a6c
--- 1/cmd.c
--- 2/cmd.c
+++ b/cmd.c
@@@ -21,6 -21,7 +21,7 @@@
  #include "ui-refs.h"
  #include "ui-repolist.h"
  #include "ui-snapshot.h"
+ #include "ui-stats.h"
  #include "ui-summary.h"
  #include "ui-tag.h"
  #include "ui-tree.h"
@@@ -104,10 -105,16 +105,15 @@@ static void refs_fn(struct cgit_contex
  
  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,
 +      cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, ctx->qry.path,
                            ctx->repo->snapshots, ctx->qry.nohead);
  }
  
+ static void stats_fn(struct cgit_context *ctx)
+ {
+       cgit_show_stats(ctx);
+ }
  static void summary_fn(struct cgit_context *ctx)
  {
        cgit_print_summary();
@@@ -144,6 -151,7 +150,7 @@@ struct cgit_cmd *cgit_get_cmd(struct cg
                def_cmd(refs, 1, 1),
                def_cmd(repolist, 0, 0),
                def_cmd(snapshot, 1, 0),
+               def_cmd(stats, 1, 1),
                def_cmd(summary, 1, 1),
                def_cmd(tag, 1, 1),
                def_cmd(tree, 1, 1),
diff --combined shared.c
index a764c4d6b25bbde782cafe159c0bef05ccc98f21,73826090669d2bfc8e6d59842e5212c7040230c4..578a54430a38d89036c2ae747e41a48d0f9666f1
+++ b/shared.c
@@@ -58,9 -58,9 +58,10 @@@ struct cgit_repo *cgit_add_repo(const c
        ret->snapshots = ctx.cfg.snapshots;
        ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
        ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
+       ret->max_stats = ctx.cfg.max_stats;
        ret->module_link = ctx.cfg.module_link;
        ret->readme = NULL;
 +      ret->mtime = -1;
        return ret;
  }
  
@@@ -267,12 -267,10 +268,12 @@@ int cgit_diff_files(const unsigned cha
        if (!load_mmfile(&file1, old_sha1) || !load_mmfile(&file2, new_sha1))
                return 1;
  
 +      memset(&diff_params, 0, sizeof(diff_params));
 +      memset(&emit_params, 0, sizeof(emit_params));
 +      memset(&emit_cb, 0, sizeof(emit_cb));
        diff_params.flags = XDF_NEED_MINIMAL;
        emit_params.ctxlen = 3;
        emit_params.flags = XDL_EMIT_FUNCNAMES;
 -      emit_params.find_func = NULL;
        emit_cb.outf = filediff_cb;
        emit_cb.priv = fn;
        xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb);
diff --combined ui-shared.c
index fba1ba6dba56a4eab7381c1cbf0065a2c641fefa,1bb30c291646c3e0d07fc5ca736024c1de6dd39d..4f2851273418dc9a81c4af942660e4923a149f5e
@@@ -281,8 -281,7 +281,8 @@@ void cgit_plain_link(char *name, char *
  }
  
  void cgit_log_link(char *name, char *title, char *class, char *head,
 -                 char *rev, char *path, int ofs, char *grep, char *pattern)
 +                 char *rev, char *path, int ofs, char *grep, char *pattern,
 +                 int showmsg)
  {
        char *delim;
  
                html(delim);
                html("ofs=");
                htmlf("%d", ofs);
 +              delim = "&";
 +      }
 +      if (showmsg) {
 +              html(delim);
 +              html("showmsg=1");
        }
        html("'>");
        html_txt(name);
@@@ -369,16 -363,19 +369,22 @@@ void cgit_patch_link(char *name, char *
        reporevlink("patch", name, title, class, head, rev, NULL);
  }
  
+ void cgit_stats_link(char *name, char *title, char *class, char *head,
+                    char *path)
+ {
+       reporevlink("stats", name, title, class, head, NULL, path);
+ }
  void cgit_object_link(struct object *obj)
  {
 -      char *page, *rev, *name;
 +      char *page, *shortrev, *fullrev, *name;
  
 +      fullrev = sha1_to_hex(obj->sha1);
 +      shortrev = xstrdup(fullrev);
 +      shortrev[10] = '\0';
        if (obj->type == OBJ_COMMIT) {
 -                cgit_commit_link(fmt("commit %s", sha1_to_hex(obj->sha1)), NULL, NULL,
 -                               ctx.qry.head, sha1_to_hex(obj->sha1));
 +                cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
 +                               ctx.qry.head, fullrev);
                return;
        } else if (obj->type == OBJ_TREE)
                page = "tree";
                page = "tag";
        else
                page = "blob";
 -      rev = sha1_to_hex(obj->sha1);
 -      name = fmt("%s %s", typename(obj->type), rev);
 -      reporevlink(page, name, NULL, NULL, ctx.qry.head, rev, NULL);
 +      name = fmt("%s %s...", typename(obj->type), shortrev);
 +      reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
  }
  
  void cgit_print_date(time_t secs, char *format, int local_time)
@@@ -557,7 -555,7 +563,7 @@@ int print_archive_ref(const char *refna
        return 0;
  }
  
- void add_hidden_formfields(int incl_head, int incl_search, char *page)
+ void cgit_add_hidden_formfields(int incl_head, int incl_search, char *page)
  {
        char *url;
  
                html_hidden("id", ctx.qry.sha1);
        if (ctx.qry.sha2)
                html_hidden("id2", ctx.qry.sha2);
 +      if (ctx.qry.showmsg)
 +              html_hidden("showmsg", "1");
  
        if (incl_search) {
                if (ctx.qry.grep)
        }
  }
  
 +const char *fallback_cmd = "repolist";
 +
  char *hc(struct cgit_cmd *cmd, const char *page)
  {
 -      return (strcmp(cmd->name, page) ? NULL : "active");
 +      return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active");
  }
  
  void cgit_print_pageheader(struct cgit_context *ctx)
  {
        struct cgit_cmd *cmd = cgit_get_cmd(ctx);
  
 +      if (!cmd && ctx->repo)
 +              fallback_cmd = "summary";
 +
        html("<table id='header'>\n");
        html("<tr>\n");
        html("<td class='logo' rowspan='2'><a href='");
                cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
                html("</td><td class='form'>");
                html("<form method='get' action=''>\n");
-               add_hidden_formfields(0, 1, ctx->qry.page);
+               cgit_add_hidden_formfields(0, 1, ctx->qry.page);
                html("<select name='h' onchange='this.form.submit();'>\n");
                for_each_branch_ref(print_branch_option, ctx->qry.head);
                html("</select> ");
                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);
 +                            NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg);
                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->max_stats)
+                       cgit_stats_link("stats", NULL, hc(cmd, "stats"),
+                                       ctx->qry.head, NULL);
                if (ctx->repo->readme)
                        reporevlink("about", "about", NULL,
                                    hc(cmd, "about"), ctx->qry.head, NULL,
                        html_url_path(cgit_fileurl(ctx->qry.repo, "log",
                                                   ctx->qry.path, NULL));
                html("'>\n");
-               add_hidden_formfields(1, 0, "log");
+               cgit_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);
@@@ -722,7 -716,8 +731,7 @@@ void cgit_print_snapshot_links(const ch
                        continue;
                filename = fmt("%s-%s%s", cgit_repobasename(repo), hex,
                               f->suffix);
 -              cgit_snapshot_link(filename, NULL, NULL, (char *)head,
 -                                 (char *)hex, filename);
 +              cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename);
                html("<br/>");
        }
  }
diff --combined ui-shared.h
index 2ab53ae215b00b749fefcc13d95ca738491ca7ae,1524dc35a3b1e28a2be61d5de38021517c83f4d0..5a3821f20e1e973f1446845d2641f5e5bc13f5a1
@@@ -19,7 -19,7 +19,7 @@@ extern void cgit_plain_link(char *name
                            char *rev, char *path);
  extern void cgit_log_link(char *name, char *title, char *class, char *head,
                          char *rev, char *path, int ofs, char *grep,
 -                        char *pattern);
 +                        char *pattern, int showmsg);
  extern void cgit_commit_link(char *name, char *title, char *class, char *head,
                             char *rev);
  extern void cgit_patch_link(char *name, char *title, char *class, char *head,
@@@ -30,6 -30,8 +30,8 @@@ extern void cgit_snapshot_link(char *na
                               char *head, char *rev, char *archivename);
  extern void cgit_diff_link(char *name, char *title, char *class, char *head,
                           char *new_rev, char *old_rev, char *path);
+ extern void cgit_stats_link(char *name, char *title, char *class, char *head,
+                           char *path);
  extern void cgit_object_link(struct object *obj);
  
  extern void cgit_print_error(char *msg);
@@@ -42,5 -44,6 +44,6 @@@ extern void cgit_print_pageheader(struc
  extern void cgit_print_filemode(unsigned short mode);
  extern void cgit_print_snapshot_links(const char *repo, const char *head,
                                      const char *hex, int snapshots);
+ extern void cgit_add_hidden_formfields(int incl_head, int incl_search,
+                                      char *page);
  #endif /* UI_SHARED_H */
diff --combined ui-tree.c
index 9876c9972e9558f82632d03280997cc8db234fac,e27e79680a58d8625308657de7a5e66280aaf4df..4b8e7a034cd7482075a9015c13584bd077d200f1
+++ b/ui-tree.c
@@@ -54,10 -54,8 +54,10 @@@ static void print_object(const unsigne
                }
                idx++;
        }
 -      htmlf(linefmt, ++lineno);
 -      html_txt(buf + start);
 +      if (start < idx) {
 +              htmlf(linefmt, ++lineno);
 +              html_txt(buf + start);
 +      }
        html("</td></tr>\n");
        html("</table>\n");
  }
@@@ -108,7 -106,10 +108,10 @@@ static int ls_item(const unsigned char 
  
        html("<td>");
        cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev,
 -                    fullpath, 0, NULL, NULL);
 +                    fullpath, 0, NULL, NULL, ctx.qry.showmsg);
+       if (ctx.repo->max_stats)
+               cgit_stats_link("stats", NULL, "button", ctx.qry.head,
+                               fullpath);
        html("</td></tr>\n");
        free(name);
        return 0;