summaryrefslogtreecommitdiffabout
Unidiff
Diffstat (more/less context) (show whitespace changes)
-rw-r--r--Makefile44
-rw-r--r--cache.c4
-rw-r--r--cgit.c24
-rw-r--r--cgit.css40
-rw-r--r--cgit.h9
-rw-r--r--cgitrc.5.txt4
-rw-r--r--cmd.c3
m---------git0
-rw-r--r--parsing.c4
-rw-r--r--shared.c5
-rwxr-xr-xtests/setup.sh8
-rwxr-xr-xtests/t0010-validate-html.sh9
-rwxr-xr-xtests/t0104-tree.sh2
-rwxr-xr-xtests/t0107-snapshot.sh22
-rw-r--r--ui-log.c88
-rw-r--r--ui-patch.c0
-rw-r--r--ui-plain.c2
-rw-r--r--ui-refs.c38
-rw-r--r--ui-repolist.c144
-rw-r--r--ui-shared.c36
-rw-r--r--ui-shared.h2
-rw-r--r--ui-snapshot.c98
-rw-r--r--ui-snapshot.h3
-rw-r--r--ui-tag.c14
-rw-r--r--ui-tree.c4
25 files changed, 466 insertions, 141 deletions
diff --git a/Makefile b/Makefile
index f426f98..a52285e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,20 +1,42 @@
1CGIT_VERSION = v0.8.1 1CGIT_VERSION = v0.8.1
2CGIT_SCRIPT_NAME = cgit.cgi 2CGIT_SCRIPT_NAME = cgit.cgi
3CGIT_SCRIPT_PATH = /var/www/htdocs/cgit 3CGIT_SCRIPT_PATH = /var/www/htdocs/cgit
4CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH)
4CGIT_CONFIG = /etc/cgitrc 5CGIT_CONFIG = /etc/cgitrc
5CACHE_ROOT = /var/cache/cgit 6CACHE_ROOT = /var/cache/cgit
6SHA1_HEADER = <openssl/sha.h> 7SHA1_HEADER = <openssl/sha.h>
7GIT_VER = 1.6.0.2 8GIT_VER = 1.6.1
8GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2 9GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2
10INSTALL = install
11
12# Define NO_STRCASESTR if you don't have strcasestr.
13#
14# Define NEEDS_LIBICONV if linking with libc is not enough (eg. Darwin).
15#
16
17#-include config.mak
18
19#
20# Platform specific tweaks
21#
22
23uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
24uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not')
25uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not')
26
27ifeq ($(uname_O),Cygwin)
28 NO_STRCASESTR = YesPlease
29 NEEDS_LIBICONV = YesPlease
30endif
9 31
10# 32#
11# Let the user override the above settings. 33# Let the user override the above settings.
12# 34#
13-include cgit.conf 35-include cgit.conf
14 36
15# 37#
16# Define a way to invoke make in subdirs quietly, shamelessly ripped 38# Define a way to invoke make in subdirs quietly, shamelessly ripped
17# from git.git 39# from git.git
18# 40#
19QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir 41QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir
20QUIET_SUBDIR1 = 42QUIET_SUBDIR1 =
@@ -88,43 +110,47 @@ VERSION: force-version
88 110
89 111
90CFLAGS += -g -Wall -Igit 112CFLAGS += -g -Wall -Igit
91CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)' 113CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)'
92CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"' 114CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"'
93CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"' 115CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"'
94CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"' 116CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"'
95CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"' 117CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"'
96 118
97ifdef NO_ICONV 119ifdef NO_ICONV
98 CFLAGS += -DNO_ICONV 120 CFLAGS += -DNO_ICONV
99endif 121endif
122ifdef NO_STRCASESTR
123 CFLAGS += -DNO_STRCASESTR
124endif
100 125
101cgit: $(OBJECTS) libgit 126cgit: $(OBJECTS) libgit
102 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS) 127 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS)
103 128
104cgit.o: VERSION 129cgit.o: VERSION
105 130
106-include $(OBJECTS:.o=.d) 131-include $(OBJECTS:.o=.d)
107 132
108libgit: 133libgit:
109 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) libgit.a 134 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 libgit.a
110 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) xdiff/lib.a 135 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 xdiff/lib.a
111 136
112test: all 137test: all
113 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all 138 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all
114 139
115install: all 140install: all
116 mkdir -p $(DESTDIR)$(CGIT_SCRIPT_PATH) 141 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH)
117 install cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 142 $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
118 install cgit.css $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.css 143 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH)
119 install cgit.png $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.png 144 $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css
145 $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png
120 146
121uninstall: 147uninstall:
122 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 148 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
123 rm -f $(CGIT_SCRIPT_PATH)/cgit.css 149 rm -f $(CGIT_DATA_PATH)/cgit.css
124 rm -f $(CGIT_SCRIPT_PATH)/cgit.png 150 rm -f $(CGIT_DATA_PATH)/cgit.png
125 151
126clean: 152clean:
127 rm -f cgit VERSION *.o *.d 153 rm -f cgit VERSION *.o *.d
128 154
129get-git: 155get-git:
130 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git 156 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git
diff --git a/cache.c b/cache.c
index 57068a1..d7a8d5a 100644
--- a/cache.c
+++ b/cache.c
@@ -407,29 +407,29 @@ int cache_ls(const char *path)
407 *name = '\0'; 407 *name = '\0';
408 } 408 }
409 slot.cache_name = fullname; 409 slot.cache_name = fullname;
410 while((ent = readdir(dir)) != NULL) { 410 while((ent = readdir(dir)) != NULL) {
411 if (strlen(ent->d_name) != 8) 411 if (strlen(ent->d_name) != 8)
412 continue; 412 continue;
413 strcpy(name, ent->d_name); 413 strcpy(name, ent->d_name);
414 if ((err = open_slot(&slot)) != 0) { 414 if ((err = open_slot(&slot)) != 0) {
415 cache_log("[cgit] unable to open path %s: %s (%d)\n", 415 cache_log("[cgit] unable to open path %s: %s (%d)\n",
416 fullname, strerror(err), err); 416 fullname, strerror(err), err);
417 continue; 417 continue;
418 } 418 }
419 printf("%s %s %10zd %s\n", 419 printf("%s %s %10"PRIuMAX" %s\n",
420 name, 420 name,
421 sprintftime("%Y-%m-%d %H:%M:%S", 421 sprintftime("%Y-%m-%d %H:%M:%S",
422 slot.cache_st.st_mtime), 422 slot.cache_st.st_mtime),
423 slot.cache_st.st_size, 423 (uintmax_t)slot.cache_st.st_size,
424 slot.buf); 424 slot.buf);
425 close_slot(&slot); 425 close_slot(&slot);
426 } 426 }
427 closedir(dir); 427 closedir(dir);
428 return 0; 428 return 0;
429} 429}
430 430
431/* Print a message to stdout */ 431/* Print a message to stdout */
432void cache_log(const char *format, ...) 432void cache_log(const char *format, ...)
433{ 433{
434 va_list args; 434 va_list args;
435 va_start(args, format); 435 va_start(args, format);
diff --git a/cgit.c b/cgit.c
index 57e11cd..608cab6 100644
--- a/cgit.c
+++ b/cgit.c
@@ -150,24 +150,28 @@ static void querystring_cb(const char *name, const char *value)
150 ctx.qry.has_sha1 = 1; 150 ctx.qry.has_sha1 = 1;
151 } else if (!strcmp(name, "id2")) { 151 } else if (!strcmp(name, "id2")) {
152 ctx.qry.sha2 = xstrdup(value); 152 ctx.qry.sha2 = xstrdup(value);
153 ctx.qry.has_sha1 = 1; 153 ctx.qry.has_sha1 = 1;
154 } else if (!strcmp(name, "ofs")) { 154 } else if (!strcmp(name, "ofs")) {
155 ctx.qry.ofs = atoi(value); 155 ctx.qry.ofs = atoi(value);
156 } else if (!strcmp(name, "path")) { 156 } else if (!strcmp(name, "path")) {
157 ctx.qry.path = trim_end(value, '/'); 157 ctx.qry.path = trim_end(value, '/');
158 } else if (!strcmp(name, "name")) { 158 } else if (!strcmp(name, "name")) {
159 ctx.qry.name = xstrdup(value); 159 ctx.qry.name = xstrdup(value);
160 } else if (!strcmp(name, "mimetype")) { 160 } else if (!strcmp(name, "mimetype")) {
161 ctx.qry.mimetype = xstrdup(value); 161 ctx.qry.mimetype = xstrdup(value);
162 } else if (!strcmp(name, "s")){
163 ctx.qry.sort = xstrdup(value);
164 } else if (!strcmp(name, "showmsg")) {
165 ctx.qry.showmsg = atoi(value);
162 } else if (!strcmp(name, "period")) { 166 } else if (!strcmp(name, "period")) {
163 ctx.qry.period = xstrdup(value); 167 ctx.qry.period = xstrdup(value);
164 } 168 }
165} 169}
166 170
167static void prepare_context(struct cgit_context *ctx) 171static void prepare_context(struct cgit_context *ctx)
168{ 172{
169 memset(ctx, 0, sizeof(ctx)); 173 memset(ctx, 0, sizeof(ctx));
170 ctx->cfg.agefile = "info/web/last-modified"; 174 ctx->cfg.agefile = "info/web/last-modified";
171 ctx->cfg.nocache = 0; 175 ctx->cfg.nocache = 0;
172 ctx->cfg.cache_size = 0; 176 ctx->cfg.cache_size = 0;
173 ctx->cfg.cache_dynamic_ttl = 5; 177 ctx->cfg.cache_dynamic_ttl = 5;
@@ -288,25 +292,24 @@ static int prepare_repo_cmd(struct cgit_context *ctx)
288 } 292 }
289 return 0; 293 return 0;
290} 294}
291 295
292static void process_request(void *cbdata) 296static void process_request(void *cbdata)
293{ 297{
294 struct cgit_context *ctx = cbdata; 298 struct cgit_context *ctx = cbdata;
295 struct cgit_cmd *cmd; 299 struct cgit_cmd *cmd;
296 300
297 cmd = cgit_get_cmd(ctx); 301 cmd = cgit_get_cmd(ctx);
298 if (!cmd) { 302 if (!cmd) {
299 ctx->page.title = "cgit error"; 303 ctx->page.title = "cgit error";
300 ctx->repo = NULL;
301 cgit_print_http_headers(ctx); 304 cgit_print_http_headers(ctx);
302 cgit_print_docstart(ctx); 305 cgit_print_docstart(ctx);
303 cgit_print_pageheader(ctx); 306 cgit_print_pageheader(ctx);
304 cgit_print_error("Invalid request"); 307 cgit_print_error("Invalid request");
305 cgit_print_docend(); 308 cgit_print_docend();
306 return; 309 return;
307 } 310 }
308 311
309 if (cmd->want_repo && !ctx->repo) { 312 if (cmd->want_repo && !ctx->repo) {
310 cgit_print_http_headers(ctx); 313 cgit_print_http_headers(ctx);
311 cgit_print_docstart(ctx); 314 cgit_print_docstart(ctx);
312 cgit_print_pageheader(ctx); 315 cgit_print_pageheader(ctx);
@@ -434,47 +437,48 @@ int main(int argc, const char **argv)
434 cgit_repolist.repos = NULL; 437 cgit_repolist.repos = NULL;
435 438
436 if (getenv("SCRIPT_NAME")) 439 if (getenv("SCRIPT_NAME"))
437 ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME")); 440 ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME"));
438 if (getenv("QUERY_STRING")) 441 if (getenv("QUERY_STRING"))
439 ctx.qry.raw = xstrdup(getenv("QUERY_STRING")); 442 ctx.qry.raw = xstrdup(getenv("QUERY_STRING"));
440 cgit_parse_args(argc, argv); 443 cgit_parse_args(argc, argv);
441 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG, 444 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG,
442 config_cb); 445 config_cb);
443 ctx.repo = NULL; 446 ctx.repo = NULL;
444 http_parse_querystring(ctx.qry.raw, querystring_cb); 447 http_parse_querystring(ctx.qry.raw, querystring_cb);
445 448
446 /* If virtual-root isn't specified in cgitrc and no url 449 /* If virtual-root isn't specified in cgitrc, lets pretend
447 * parameter is specified on the querystring, lets pretend 450 * that virtual-root equals SCRIPT_NAME.
448 * that virtualroot equals SCRIPT_NAME and use PATH_INFO as
449 * url. This allows cgit to work with virtual urls without
450 * the need for rewriterules in the webserver (as long as
451 * PATH_INFO is included in the cache lookup key).
452 */ 451 */
453 if (!ctx.cfg.virtual_root && !ctx.qry.url) { 452 if (!ctx.cfg.virtual_root)
454 ctx.cfg.virtual_root = ctx.cfg.script_name; 453 ctx.cfg.virtual_root = ctx.cfg.script_name;
454
455 /* If no url parameter is specified on the querystring, lets
456 * use PATH_INFO as url. This allows cgit to work with virtual
457 * urls without the need for rewriterules in the webserver (as
458 * long as PATH_INFO is included in the cache lookup key).
459 */
455 path = getenv("PATH_INFO"); 460 path = getenv("PATH_INFO");
456 if (path) { 461 if (!ctx.qry.url && path) {
457 if (path[0] == '/') 462 if (path[0] == '/')
458 path++; 463 path++;
459 ctx.qry.url = xstrdup(path); 464 ctx.qry.url = xstrdup(path);
460 if (ctx.qry.raw) { 465 if (ctx.qry.raw) {
461 qry = ctx.qry.raw; 466 qry = ctx.qry.raw;
462 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 467 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
463 free(qry); 468 free(qry);
464 } else 469 } else
465 ctx.qry.raw = ctx.qry.url; 470 ctx.qry.raw = ctx.qry.url;
466 cgit_parse_url(ctx.qry.url); 471 cgit_parse_url(ctx.qry.url);
467 } 472 }
468 }
469 473
470 ttl = calc_ttl(); 474 ttl = calc_ttl();
471 ctx.page.expires += ttl*60; 475 ctx.page.expires += ttl*60;
472 if (ctx.cfg.nocache) 476 if (ctx.cfg.nocache)
473 ctx.cfg.cache_size = 0; 477 ctx.cfg.cache_size = 0;
474 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 478 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
475 ctx.qry.raw, ttl, process_request, &ctx); 479 ctx.qry.raw, ttl, process_request, &ctx);
476 if (err) 480 if (err)
477 cgit_print_error(fmt("Error processing page: %s (%d)", 481 cgit_print_error(fmt("Error processing page: %s (%d)",
478 strerror(err), err)); 482 strerror(err), err));
479 return err; 483 return err;
480} 484}
diff --git a/cgit.css b/cgit.css
index ef30fbf..e8214de 100644
--- a/cgit.css
+++ b/cgit.css
@@ -111,47 +111,62 @@ div.content {
111 111
112 112
113table.list { 113table.list {
114 width: 100%; 114 width: 100%;
115 border: none; 115 border: none;
116 border-collapse: collapse; 116 border-collapse: collapse;
117} 117}
118 118
119table.list tr { 119table.list tr {
120 background: white; 120 background: white;
121} 121}
122 122
123table.list tr.logheader {
124 background: #eee;
125}
126
123table.list tr:hover { 127table.list tr:hover {
124 background: #eee; 128 background: #eee;
125} 129}
126 130
127table.list tr.nohover:hover { 131table.list tr.nohover:hover {
128 background: white; 132 background: white;
129} 133}
130 134
131table.list th { 135table.list th {
132 font-weight: bold; 136 font-weight: bold;
133 /* color: #888; 137 /* color: #888;
134 border-top: dashed 1px #888; 138 border-top: dashed 1px #888;
135 border-bottom: dashed 1px #888; 139 border-bottom: dashed 1px #888;
136 */ 140 */
137 padding: 0.1em 0.5em 0.05em 0.5em; 141 padding: 0.1em 0.5em 0.05em 0.5em;
138 vertical-align: baseline; 142 vertical-align: baseline;
139} 143}
140 144
141table.list td { 145table.list td {
142 border: none; 146 border: none;
143 padding: 0.1em 0.5em 0.1em 0.5em; 147 padding: 0.1em 0.5em 0.1em 0.5em;
144} 148}
145 149
150table.list td.logsubject {
151 font-family: monospace;
152 font-weight: bold;
153}
154
155table.list td.logmsg {
156 font-family: monospace;
157 white-space: pre;
158 padding: 1em 0em 2em 0em;
159}
160
146table.list td a { 161table.list td a {
147 color: black; 162 color: black;
148} 163}
149 164
150table.list td a:hover { 165table.list td a:hover {
151 color: #00f; 166 color: #00f;
152} 167}
153 168
154img { 169img {
155 border: none; 170 border: none;
156} 171}
157 172
@@ -447,24 +462,48 @@ span.age-months {
447 color: #888; 462 color: #888;
448} 463}
449 464
450span.age-years { 465span.age-years {
451 color: #bbb; 466 color: #bbb;
452} 467}
453div.footer { 468div.footer {
454 margin-top: 0.5em; 469 margin-top: 0.5em;
455 text-align: center; 470 text-align: center;
456 font-size: 80%; 471 font-size: 80%;
457 color: #ccc; 472 color: #ccc;
458} 473}
474a.branch-deco {
475 margin: 0px 0.5em;
476 padding: 0px 0.25em;
477 background-color: #88ff88;
478 border: solid 1px #007700;
479}
480a.tag-deco {
481 margin: 0px 0.5em;
482 padding: 0px 0.25em;
483 background-color: #ffff88;
484 border: solid 1px #777700;
485}
486a.remote-deco {
487 margin: 0px 0.5em;
488 padding: 0px 0.25em;
489 background-color: #ccccff;
490 border: solid 1px #000077;
491}
492a.deco {
493 margin: 0px 0.5em;
494 padding: 0px 0.25em;
495 background-color: #ff8888;
496 border: solid 1px #770000;
497}
459table.stats { 498table.stats {
460 border: solid 1px black; 499 border: solid 1px black;
461 border-collapse: collapse; 500 border-collapse: collapse;
462} 501}
463 502
464table.stats th { 503table.stats th {
465 text-align: left; 504 text-align: left;
466 padding: 1px 0.5em; 505 padding: 1px 0.5em;
467 background-color: #eee; 506 background-color: #eee;
468 border: solid 1px black; 507 border: solid 1px black;
469} 508}
470 509
@@ -523,13 +562,12 @@ table.hgraph th {
523 padding: 1px 0.5em; 562 padding: 1px 0.5em;
524} 563}
525 564
526table.hgraph td { 565table.hgraph td {
527 vertical-align: center; 566 vertical-align: center;
528 padding: 2px 2px; 567 padding: 2px 2px;
529} 568}
530 569
531table.hgraph div.bar { 570table.hgraph div.bar {
532 background-color: #eee; 571 background-color: #eee;
533 height: 1em; 572 height: 1em;
534} 573}
535
diff --git a/cgit.h b/cgit.h
index f2cb671..4fe94c6 100644
--- a/cgit.h
+++ b/cgit.h
@@ -53,24 +53,25 @@ struct cgit_repo {
53 char *path; 53 char *path;
54 char *desc; 54 char *desc;
55 char *owner; 55 char *owner;
56 char *defbranch; 56 char *defbranch;
57 char *group; 57 char *group;
58 char *module_link; 58 char *module_link;
59 char *readme; 59 char *readme;
60 char *clone_url; 60 char *clone_url;
61 int snapshots; 61 int snapshots;
62 int enable_log_filecount; 62 int enable_log_filecount;
63 int enable_log_linecount; 63 int enable_log_linecount;
64 int max_stats; 64 int max_stats;
65 time_t mtime;
65}; 66};
66 67
67struct cgit_repolist { 68struct cgit_repolist {
68 int length; 69 int length;
69 int count; 70 int count;
70 struct cgit_repo *repos; 71 struct cgit_repo *repos;
71}; 72};
72 73
73struct commitinfo { 74struct commitinfo {
74 struct commit *commit; 75 struct commit *commit;
75 char *author; 76 char *author;
76 char *author_email; 77 char *author_email;
@@ -114,24 +115,26 @@ struct cgit_query {
114 char *search; 115 char *search;
115 char *grep; 116 char *grep;
116 char *head; 117 char *head;
117 char *sha1; 118 char *sha1;
118 char *sha2; 119 char *sha2;
119 char *path; 120 char *path;
120 char *name; 121 char *name;
121 char *mimetype; 122 char *mimetype;
122 char *url; 123 char *url;
123 char *period; 124 char *period;
124 int ofs; 125 int ofs;
125 int nohead; 126 int nohead;
127 char *sort;
128 int showmsg;
126}; 129};
127 130
128struct cgit_config { 131struct cgit_config {
129 char *agefile; 132 char *agefile;
130 char *cache_root; 133 char *cache_root;
131 char *clone_prefix; 134 char *clone_prefix;
132 char *css; 135 char *css;
133 char *favicon; 136 char *favicon;
134 char *footer; 137 char *footer;
135 char *index_header; 138 char *index_header;
136 char *index_info; 139 char *index_info;
137 char *logo; 140 char *logo;
@@ -227,20 +230,14 @@ extern void cgit_diff_tree(const unsigned char *old_sha1,
227extern void cgit_diff_commit(struct commit *commit, filepair_fn fn); 230extern void cgit_diff_commit(struct commit *commit, filepair_fn fn);
228 231
229extern char *fmt(const char *format,...); 232extern char *fmt(const char *format,...);
230 233
231extern struct commitinfo *cgit_parse_commit(struct commit *commit); 234extern struct commitinfo *cgit_parse_commit(struct commit *commit);
232extern struct taginfo *cgit_parse_tag(struct tag *tag); 235extern struct taginfo *cgit_parse_tag(struct tag *tag);
233extern void cgit_parse_url(const char *url); 236extern void cgit_parse_url(const char *url);
234 237
235extern const char *cgit_repobasename(const char *reponame); 238extern const char *cgit_repobasename(const char *reponame);
236 239
237extern int cgit_parse_snapshots_mask(const char *str); 240extern int cgit_parse_snapshots_mask(const char *str);
238 241
239/* libgit.a either links against or compiles its own implementation of
240 * strcasestr(), and we'd like to reuse it. Simply re-declaring it
241 * seems to do the trick.
242 */
243extern char *strcasestr(const char *haystack, const char *needle);
244
245 242
246#endif /* CGIT_H */ 243#endif /* CGIT_H */
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 0bbbea3..09f56a6 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -293,26 +293,26 @@ max-stats=quarter
293# Set the title and heading of the repository index page 293# Set the title and heading of the repository index page
294root-title=foobar.com git repositories 294root-title=foobar.com git repositories
295 295
296 296
297# Set a subheading for the repository index page 297# Set a subheading for the repository index page
298root-desc=tracking the foobar development 298root-desc=tracking the foobar development
299 299
300 300
301# Include some more info about foobar.com on the index page 301# Include some more info about foobar.com on the index page
302root-readme=/var/www/htdocs/about.html 302root-readme=/var/www/htdocs/about.html
303 303
304 304
305# Allow download of tar.gz, tar.bz and zip-files 305# Allow download of tar.gz, tar.bz2 and zip-files
306snapshots=tar.gz tar.bz zip 306snapshots=tar.gz tar.bz2 zip
307 307
308 308
309## 309##
310## List of repositories. 310## List of repositories.
311## 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
312## displayed under a group heading 312## displayed under a group heading
313## 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')
314## and included like this: 314## and included like this:
315## include=/etc/cgitrepos 315## include=/etc/cgitrepos
316## 316##
317 317
318 318
diff --git a/cmd.c b/cmd.c
index 763a558..cf97da7 100644
--- a/cmd.c
+++ b/cmd.c
@@ -96,26 +96,25 @@ static void patch_fn(struct cgit_context *ctx)
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, ctx->qry.path,
109 cgit_repobasename(ctx->repo->url), ctx->qry.path,
110 ctx->repo->snapshots, ctx->qry.nohead); 109 ctx->repo->snapshots, ctx->qry.nohead);
111} 110}
112 111
113static void stats_fn(struct cgit_context *ctx) 112static void stats_fn(struct cgit_context *ctx)
114{ 113{
115 cgit_show_stats(ctx); 114 cgit_show_stats(ctx);
116} 115}
117 116
118static void summary_fn(struct cgit_context *ctx) 117static void summary_fn(struct cgit_context *ctx)
119{ 118{
120 cgit_print_summary(); 119 cgit_print_summary();
121} 120}
diff --git a/git b/git
Subproject 97a7a82f199f165f85fe39a3c318b18c621e633 Subproject 8104ebfe8276657ee803cca7eb8665a78cf3ef8
diff --git a/parsing.c b/parsing.c
index c8f3048..f3f3b15 100644
--- a/parsing.c
+++ b/parsing.c
@@ -87,38 +87,42 @@ char *parse_user(char *t, char **name, char **email, unsigned long *date)
87 } else if (mode == 3 && isdigit(*p)) { 87 } else if (mode == 3 && isdigit(*p)) {
88 *date = atol(p); 88 *date = atol(p);
89 mode++; 89 mode++;
90 } else if (*p == '\n') { 90 } else if (*p == '\n') {
91 p++; 91 p++;
92 break; 92 break;
93 } 93 }
94 p++; 94 p++;
95 } 95 }
96 return p; 96 return p;
97} 97}
98 98
99#ifdef NO_ICONV
100#define reencode(a, b, c)
101#else
99const char *reencode(char **txt, const char *src_enc, const char *dst_enc) 102const char *reencode(char **txt, const char *src_enc, const char *dst_enc)
100{ 103{
101 char *tmp; 104 char *tmp;
102 105
103 if (!txt || !*txt || !src_enc || !dst_enc) 106 if (!txt || !*txt || !src_enc || !dst_enc)
104 return *txt; 107 return *txt;
105 108
106 tmp = reencode_string(*txt, src_enc, dst_enc); 109 tmp = reencode_string(*txt, src_enc, dst_enc);
107 if (tmp) { 110 if (tmp) {
108 free(*txt); 111 free(*txt);
109 *txt = tmp; 112 *txt = tmp;
110 } 113 }
111 return *txt; 114 return *txt;
112} 115}
116#endif
113 117
114struct commitinfo *cgit_parse_commit(struct commit *commit) 118struct commitinfo *cgit_parse_commit(struct commit *commit)
115{ 119{
116 struct commitinfo *ret; 120 struct commitinfo *ret;
117 char *p = commit->buffer, *t = commit->buffer; 121 char *p = commit->buffer, *t = commit->buffer;
118 122
119 ret = xmalloc(sizeof(*ret)); 123 ret = xmalloc(sizeof(*ret));
120 ret->commit = commit; 124 ret->commit = commit;
121 ret->author = NULL; 125 ret->author = NULL;
122 ret->author_email = NULL; 126 ret->author_email = NULL;
123 ret->committer = NULL; 127 ret->committer = NULL;
124 ret->committer_email = NULL; 128 ret->committer_email = NULL;
diff --git a/shared.c b/shared.c
index 7382609..578a544 100644
--- a/shared.c
+++ b/shared.c
@@ -52,24 +52,25 @@ struct cgit_repo *cgit_add_repo(const char *url)
52 ret->name = ret->url; 52 ret->name = ret->url;
53 ret->path = NULL; 53 ret->path = NULL;
54 ret->desc = "[no description]"; 54 ret->desc = "[no description]";
55 ret->owner = NULL; 55 ret->owner = NULL;
56 ret->group = ctx.cfg.repo_group; 56 ret->group = ctx.cfg.repo_group;
57 ret->defbranch = "master"; 57 ret->defbranch = "master";
58 ret->snapshots = ctx.cfg.snapshots; 58 ret->snapshots = ctx.cfg.snapshots;
59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount; 59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount; 60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
61 ret->max_stats = ctx.cfg.max_stats; 61 ret->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 ret->mtime = -1;
64 return ret; 65 return ret;
65} 66}
66 67
67struct cgit_repo *cgit_get_repoinfo(const char *url) 68struct cgit_repo *cgit_get_repoinfo(const char *url)
68{ 69{
69 int i; 70 int i;
70 struct cgit_repo *repo; 71 struct cgit_repo *repo;
71 72
72 for (i=0; i<cgit_repolist.count; i++) { 73 for (i=0; i<cgit_repolist.count; i++) {
73 repo = &cgit_repolist.repos[i]; 74 repo = &cgit_repolist.repos[i];
74 if (!strcmp(repo->url, url)) 75 if (!strcmp(repo->url, url))
75 return repo; 76 return repo;
@@ -258,28 +259,30 @@ int filediff_cb(void *priv, mmbuffer_t *mb, int nbuf)
258int cgit_diff_files(const unsigned char *old_sha1, 259int cgit_diff_files(const unsigned char *old_sha1,
259 const unsigned char *new_sha1, 260 const unsigned char *new_sha1,
260 linediff_fn fn) 261 linediff_fn fn)
261{ 262{
262 mmfile_t file1, file2; 263 mmfile_t file1, file2;
263 xpparam_t diff_params; 264 xpparam_t diff_params;
264 xdemitconf_t emit_params; 265 xdemitconf_t emit_params;
265 xdemitcb_t emit_cb; 266 xdemitcb_t emit_cb;
266 267
267 if (!load_mmfile(&file1, old_sha1) || !load_mmfile(&file2, new_sha1)) 268 if (!load_mmfile(&file1, old_sha1) || !load_mmfile(&file2, new_sha1))
268 return 1; 269 return 1;
269 270
271 memset(&diff_params, 0, sizeof(diff_params));
272 memset(&emit_params, 0, sizeof(emit_params));
273 memset(&emit_cb, 0, sizeof(emit_cb));
270 diff_params.flags = XDF_NEED_MINIMAL; 274 diff_params.flags = XDF_NEED_MINIMAL;
271 emit_params.ctxlen = 3; 275 emit_params.ctxlen = 3;
272 emit_params.flags = XDL_EMIT_FUNCNAMES; 276 emit_params.flags = XDL_EMIT_FUNCNAMES;
273 emit_params.find_func = NULL;
274 emit_cb.outf = filediff_cb; 277 emit_cb.outf = filediff_cb;
275 emit_cb.priv = fn; 278 emit_cb.priv = fn;
276 xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb); 279 xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb);
277 return 0; 280 return 0;
278} 281}
279 282
280void cgit_diff_tree(const unsigned char *old_sha1, 283void cgit_diff_tree(const unsigned char *old_sha1,
281 const unsigned char *new_sha1, 284 const unsigned char *new_sha1,
282 filepair_fn fn, const char *prefix) 285 filepair_fn fn, const char *prefix)
283{ 286{
284 struct diff_options opt; 287 struct diff_options opt;
285 int ret; 288 int ret;
diff --git a/tests/setup.sh b/tests/setup.sh
index 1457dd5..30f90d5 100755
--- a/tests/setup.sh
+++ b/tests/setup.sh
@@ -16,29 +16,31 @@
16# run_test 'repo summary' 'cgit_url "/foo" | tidy -e' 16# run_test 'repo summary' 'cgit_url "/foo" | tidy -e'
17 17
18 18
19mkrepo() { 19mkrepo() {
20 name=$1 20 name=$1
21 count=$2 21 count=$2
22 dir=$PWD 22 dir=$PWD
23 test -d $name && return 23 test -d $name && return
24 printf "Creating testrepo %s\n" $name 24 printf "Creating testrepo %s\n" $name
25 mkdir -p $name 25 mkdir -p $name
26 cd $name 26 cd $name
27 git init 27 git init
28 for ((n=1; n<=count; n++)) 28 n=1
29 while test $n -le $count
29 do 30 do
30 echo $n >file-$n 31 echo $n >file-$n
31 git add file-$n 32 git add file-$n
32 git commit -m "commit $n" 33 git commit -m "commit $n"
34 n=$(expr $n + 1)
33 done 35 done
34 if test "$3" = "testplus" 36 if test "$3" = "testplus"
35 then 37 then
36 echo "hello" >a+b 38 echo "hello" >a+b
37 git add a+b 39 git add a+b
38 git commit -m "add a+b" 40 git commit -m "add a+b"
39 git branch "1+2" 41 git branch "1+2"
40 fi 42 fi
41 cd $dir 43 cd $dir
42} 44}
43 45
44setup_repos() 46setup_repos()
@@ -92,35 +94,35 @@ tests_done()
92 if test $test_failed -gt 0 94 if test $test_failed -gt 0
93 then 95 then
94 printf "test: *** %s failure(s), logfile=%s\n" \ 96 printf "test: *** %s failure(s), logfile=%s\n" \
95 $test_failed "$(pwd)/test-output.log" 97 $test_failed "$(pwd)/test-output.log"
96 false 98 false
97 fi 99 fi
98} 100}
99 101
100run_test() 102run_test()
101{ 103{
102 desc=$1 104 desc=$1
103 script=$2 105 script=$2
104 ((test_count++)) 106 test_count=$(expr $test_count + 1)
105 printf "\ntest %d: name='%s'\n" $test_count "$desc" >>test-output.log 107 printf "\ntest %d: name='%s'\n" $test_count "$desc" >>test-output.log
106 printf "test %d: eval='%s'\n" $test_count "$2" >>test-output.log 108 printf "test %d: eval='%s'\n" $test_count "$2" >>test-output.log
107 eval "$2" >>test-output.log 2>>test-output.log 109 eval "$2" >>test-output.log 2>>test-output.log
108 res=$? 110 res=$?
109 printf "test %d: exitcode=%d\n" $test_count $res >>test-output.log 111 printf "test %d: exitcode=%d\n" $test_count $res >>test-output.log
110 if test $res = 0 112 if test $res = 0
111 then 113 then
112 printf " %2d) %-60s [ok]\n" $test_count "$desc" 114 printf " %2d) %-60s [ok]\n" $test_count "$desc"
113 else 115 else
114 ((test_failed++)) 116 test_failed=$(expr $test_failed + 1)
115 printf " %2d) %-60s [failed]\n" $test_count "$desc" 117 printf " %2d) %-60s [failed]\n" $test_count "$desc"
116 fi 118 fi
117} 119}
118 120
119cgit_query() 121cgit_query()
120{ 122{
121 CGIT_CONFIG="$PWD/trash/cgitrc" QUERY_STRING="$1" "$PWD/../cgit" 123 CGIT_CONFIG="$PWD/trash/cgitrc" QUERY_STRING="$1" "$PWD/../cgit"
122} 124}
123 125
124cgit_url() 126cgit_url()
125{ 127{
126 CGIT_CONFIG="$PWD/trash/cgitrc" QUERY_STRING="url=$1" "$PWD/../cgit" 128 CGIT_CONFIG="$PWD/trash/cgitrc" QUERY_STRING="url=$1" "$PWD/../cgit"
diff --git a/tests/t0010-validate-html.sh b/tests/t0010-validate-html.sh
index 94aa52b..3fe4800 100755
--- a/tests/t0010-validate-html.sh
+++ b/tests/t0010-validate-html.sh
@@ -1,34 +1,41 @@
1#!/bin/sh 1#!/bin/sh
2 2
3. ./setup.sh 3. ./setup.sh
4 4
5 5
6test_url() 6test_url()
7{ 7{
8 tidy_opt="-eq" 8 tidy_opt="-eq"
9 test -z "$NO_TIDY_WARNINGS" || tidy_opt+=" --show-warnings no" 9 test -z "$NO_TIDY_WARNINGS" || tidy_opt+=" --show-warnings no"
10 cgit_url "$1" >trash/tidy-$test_count || return 10 cgit_url "$1" >trash/tidy-$test_count || return
11 sed -ie "1,4d" trash/tidy-$test_count || return 11 sed -ie "1,4d" trash/tidy-$test_count || return
12 tidy $tidy_opt trash/tidy-$test_count 12 "$tidy" $tidy_opt trash/tidy-$test_count
13 rc=$? 13 rc=$?
14 14
15 # tidy returns with exitcode 1 on warnings, 2 on error 15 # tidy returns with exitcode 1 on warnings, 2 on error
16 if test $rc = 2 16 if test $rc = 2
17 then 17 then
18 false 18 false
19 else 19 else
20 : 20 :
21 fi 21 fi
22} 22}
23 23
24prepare_tests 'Validate html with tidy' 24prepare_tests 'Validate html with tidy'
25 25
26tidy=`which tidy`
27test -n "$tidy" || {
28 echo "Skipping tests: tidy not found"
29 tests_done
30 exit
31}
32
26run_test 'index page' 'test_url ""' 33run_test 'index page' 'test_url ""'
27run_test 'foo' 'test_url "foo"' 34run_test 'foo' 'test_url "foo"'
28run_test 'foo/log' 'test_url "foo/log"' 35run_test 'foo/log' 'test_url "foo/log"'
29run_test 'foo/tree' 'test_url "foo/tree"' 36run_test 'foo/tree' 'test_url "foo/tree"'
30run_test 'foo/tree/file-1' 'test_url "foo/tree/file-1"' 37run_test 'foo/tree/file-1' 'test_url "foo/tree/file-1"'
31run_test 'foo/commit' 'test_url "foo/commit"' 38run_test 'foo/commit' 'test_url "foo/commit"'
32run_test 'foo/diff' 'test_url "foo/diff"' 39run_test 'foo/diff' 'test_url "foo/diff"'
33 40
34tests_done 41tests_done
diff --git a/tests/t0104-tree.sh b/tests/t0104-tree.sh
index 0d62cc8..33f4eb0 100755
--- a/tests/t0104-tree.sh
+++ b/tests/t0104-tree.sh
@@ -6,25 +6,25 @@ prepare_tests "Check content on tree page"
6 6
7run_test 'generate bar/tree' 'cgit_url "bar/tree" >trash/tmp' 7run_test 'generate bar/tree' 'cgit_url "bar/tree" >trash/tmp'
8run_test 'find file-1' 'grep -e "file-1" trash/tmp' 8run_test 'find file-1' 'grep -e "file-1" trash/tmp'
9run_test 'find file-50' 'grep -e "file-50" trash/tmp' 9run_test 'find file-50' 'grep -e "file-50" trash/tmp'
10 10
11run_test 'generate bar/tree/file-50' 'cgit_url "bar/tree/file-50" >trash/tmp' 11run_test 'generate bar/tree/file-50' 'cgit_url "bar/tree/file-50" >trash/tmp'
12 12
13run_test 'find line 1' ' 13run_test 'find line 1' '
14 grep -e "<a id=.n1. name=.n1. href=.#n1.>1</a>" trash/tmp 14 grep -e "<a id=.n1. name=.n1. href=.#n1.>1</a>" trash/tmp
15' 15'
16 16
17run_test 'no line 2' ' 17run_test 'no line 2' '
18 grep -e "<a id=.n2. name=.n2. href=.#n2.>2</a>" trash/tmp 18 ! grep -e "<a id=.n2. name=.n2. href=.#n2.>2</a>" trash/tmp
19' 19'
20 20
21run_test 'generate foo+bar/tree' 'cgit_url "foo%2bbar/tree" >trash/tmp' 21run_test 'generate foo+bar/tree' 'cgit_url "foo%2bbar/tree" >trash/tmp'
22 22
23run_test 'verify a+b link' ' 23run_test 'verify a+b link' '
24 grep -e "/foo+bar/tree/a+b" trash/tmp 24 grep -e "/foo+bar/tree/a+b" trash/tmp
25' 25'
26 26
27run_test 'generate foo+bar/tree?h=1+2' 'cgit_url "foo%2bbar/tree&h=1%2b2" >trash/tmp' 27run_test 'generate foo+bar/tree?h=1+2' 'cgit_url "foo%2bbar/tree&h=1%2b2" >trash/tmp'
28 28
29run_test 'verify a+b?h=1+2 link' ' 29run_test 'verify a+b?h=1+2 link' '
30 grep -e "/foo+bar/tree/a+b?h=1%2b2" trash/tmp 30 grep -e "/foo+bar/tree/a+b?h=1%2b2" trash/tmp
diff --git a/tests/t0107-snapshot.sh b/tests/t0107-snapshot.sh
index d97c465..8ab4912 100755
--- a/tests/t0107-snapshot.sh
+++ b/tests/t0107-snapshot.sh
@@ -1,39 +1,39 @@
1#!/bin/sh 1#!/bin/sh
2 2
3. ./setup.sh 3. ./setup.sh
4 4
5prepare_tests "Verify snapshot" 5prepare_tests "Verify snapshot"
6 6
7run_test 'get foo/snapshot/test.tar.gz' ' 7run_test 'get foo/snapshot/master.tar.gz' '
8 cgit_url "foo/snapshot/test.tar.gz" >trash/tmp 8 cgit_url "foo/snapshot/master.tar.gz" >trash/tmp
9' 9'
10 10
11run_test 'check html headers' ' 11run_test 'check html headers' '
12 head -n 1 trash/tmp | 12 head -n 1 trash/tmp |
13 grep -e "Content-Type: application/x-tar" && 13 grep -e "Content-Type: application/x-gzip" &&
14 14
15 head -n 2 trash/tmp | 15 head -n 2 trash/tmp |
16 grep -e "Content-Disposition: inline; filename=.test.tar.gz." 16 grep -e "Content-Disposition: inline; filename=.master.tar.gz."
17' 17'
18 18
19run_test 'strip off the header lines' ' 19run_test 'strip off the header lines' '
20 tail -n +6 trash/tmp > trash/test.tar.gz 20 tail -n +6 trash/tmp > trash/master.tar.gz
21' 21'
22 22
23run_test 'verify gzip format' 'gunzip --test trash/test.tar.gz' 23run_test 'verify gzip format' 'gunzip --test trash/master.tar.gz'
24run_test 'untar' ' 24run_test 'untar' '
25 rm -rf trash/foo && 25 rm -rf trash/master &&
26 tar -xf trash/test.tar.gz -C trash 26 tar -xf trash/master.tar.gz -C trash
27' 27'
28 28
29run_test 'count files' ' 29run_test 'count files' '
30 c=$(ls -1 trash/foo/ | wc -l) && 30 c=$(ls -1 trash/master/ | wc -l) &&
31 test $c = 5 31 test $c = 5
32' 32'
33 33
34run_test 'verify untarred file-5' ' 34run_test 'verify untarred file-5' '
35 grep -e "^5$" trash/foo/file-5 && 35 grep -e "^5$" trash/master/file-5 &&
36 test $(cat trash/foo/file-5 | wc -l) = 1 36 test $(cat trash/master/file-5 | wc -l) = 1
37' 37'
38 38
39tests_done 39tests_done
diff --git a/ui-log.c b/ui-log.c
index 8dd8b89..3202848 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -22,95 +22,165 @@ void count_lines(char *line, int size)
22 22
23 else if (line[0] == '-') 23 else if (line[0] == '-')
24 rem_lines++; 24 rem_lines++;
25} 25}
26 26
27void inspect_files(struct diff_filepair *pair) 27void inspect_files(struct diff_filepair *pair)
28{ 28{
29 files++; 29 files++;
30 if (ctx.repo->enable_log_linecount) 30 if (ctx.repo->enable_log_linecount)
31 cgit_diff_files(pair->one->sha1, pair->two->sha1, count_lines); 31 cgit_diff_files(pair->one->sha1, pair->two->sha1, count_lines);
32} 32}
33 33
34void show_commit_decorations(struct commit *commit)
35{
36 struct name_decoration *deco;
37 static char buf[1024];
38
39 buf[sizeof(buf) - 1] = 0;
40 deco = lookup_decoration(&name_decoration, &commit->object);
41 while (deco) {
42 if (!prefixcmp(deco->name, "refs/heads/")) {
43 strncpy(buf, deco->name + 11, sizeof(buf) - 1);
44 cgit_log_link(buf, NULL, "branch-deco", buf, NULL, NULL,
45 0, NULL, NULL, ctx.qry.showmsg);
46 }
47 else if (!prefixcmp(deco->name, "tag: refs/tags/")) {
48 strncpy(buf, deco->name + 15, sizeof(buf) - 1);
49 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf);
50 }
51 else if (!prefixcmp(deco->name, "refs/remotes/")) {
52 strncpy(buf, deco->name + 13, sizeof(buf) - 1);
53 cgit_log_link(buf, NULL, "remote-deco", NULL,
54 sha1_to_hex(commit->object.sha1), NULL,
55 0, NULL, NULL, ctx.qry.showmsg);
56 }
57 else {
58 strncpy(buf, deco->name, sizeof(buf) - 1);
59 cgit_commit_link(buf, NULL, "deco", ctx.qry.head,
60 sha1_to_hex(commit->object.sha1));
61 }
62 deco = deco->next;
63 }
64}
65
34void print_commit(struct commit *commit) 66void print_commit(struct commit *commit)
35{ 67{
36 struct commitinfo *info; 68 struct commitinfo *info;
37 char *tmp; 69 char *tmp;
70 int cols = 2;
38 71
39 info = cgit_parse_commit(commit); 72 info = cgit_parse_commit(commit);
40 html("<tr><td>"); 73 htmlf("<tr%s><td>",
74 ctx.qry.showmsg ? " class='logheader'" : "");
41 tmp = fmt("id=%s", sha1_to_hex(commit->object.sha1)); 75 tmp = fmt("id=%s", sha1_to_hex(commit->object.sha1));
42 tmp = cgit_pageurl(ctx.repo->url, "commit", tmp); 76 tmp = cgit_pageurl(ctx.repo->url, "commit", tmp);
43 html_link_open(tmp, NULL, NULL); 77 html_link_open(tmp, NULL, NULL);
44 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE); 78 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE);
45 html_link_close(); 79 html_link_close();
46 html("</td><td>"); 80 htmlf("</td><td%s>",
81 ctx.qry.showmsg ? " class='logsubject'" : "");
47 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head, 82 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head,
48 sha1_to_hex(commit->object.sha1)); 83 sha1_to_hex(commit->object.sha1));
84 show_commit_decorations(commit);
49 html("</td><td>"); 85 html("</td><td>");
50 html_txt(info->author); 86 html_txt(info->author);
51 if (ctx.repo->enable_log_filecount) { 87 if (ctx.repo->enable_log_filecount) {
52 files = 0; 88 files = 0;
53 add_lines = 0; 89 add_lines = 0;
54 rem_lines = 0; 90 rem_lines = 0;
55 cgit_diff_commit(commit, inspect_files); 91 cgit_diff_commit(commit, inspect_files);
56 html("</td><td>"); 92 html("</td><td>");
57 htmlf("%d", files); 93 htmlf("%d", files);
58 if (ctx.repo->enable_log_linecount) { 94 if (ctx.repo->enable_log_linecount) {
59 html("</td><td>"); 95 html("</td><td>");
60 htmlf("-%d/+%d", rem_lines, add_lines); 96 htmlf("-%d/+%d", rem_lines, add_lines);
61 } 97 }
62 } 98 }
63 html("</td></tr>\n"); 99 html("</td></tr>\n");
100 if (ctx.qry.showmsg) {
101 if (ctx.repo->enable_log_filecount) {
102 cols++;
103 if (ctx.repo->enable_log_linecount)
104 cols++;
105 }
106 htmlf("<tr class='nohover'><td/><td colspan='%d' class='logmsg'>",
107 cols);
108 html_txt(info->msg);
109 html("</td></tr>\n");
110 }
64 cgit_free_commitinfo(info); 111 cgit_free_commitinfo(info);
65} 112}
66 113
114static const char *disambiguate_ref(const char *ref)
115{
116 unsigned char sha1[20];
117 const char *longref;
118
119 longref = fmt("refs/heads/%s", ref);
120 if (get_sha1(longref, sha1) == 0)
121 return longref;
122
123 return ref;
124}
67 125
68void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern, 126void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern,
69 char *path, int pager) 127 char *path, int pager)
70{ 128{
71 struct rev_info rev; 129 struct rev_info rev;
72 struct commit *commit; 130 struct commit *commit;
73 const char *argv[] = {NULL, tip, NULL, NULL, NULL}; 131 const char *argv[] = {NULL, NULL, NULL, NULL, NULL};
74 int argc = 2; 132 int argc = 2;
75 int i, columns = 3; 133 int i, columns = 3;
76 134
77 if (!tip) 135 if (!tip)
78 argv[1] = ctx.qry.head; 136 tip = ctx.qry.head;
137
138 argv[1] = disambiguate_ref(tip);
79 139
80 if (grep && pattern && (!strcmp(grep, "grep") || 140 if (grep && pattern && (!strcmp(grep, "grep") ||
81 !strcmp(grep, "author") || 141 !strcmp(grep, "author") ||
82 !strcmp(grep, "committer"))) 142 !strcmp(grep, "committer")))
83 argv[argc++] = fmt("--%s=%s", grep, pattern); 143 argv[argc++] = fmt("--%s=%s", grep, pattern);
84 144
85 if (path) { 145 if (path) {
86 argv[argc++] = "--"; 146 argv[argc++] = "--";
87 argv[argc++] = path; 147 argv[argc++] = path;
88 } 148 }
89 init_revisions(&rev, NULL); 149 init_revisions(&rev, NULL);
90 rev.abbrev = DEFAULT_ABBREV; 150 rev.abbrev = DEFAULT_ABBREV;
91 rev.commit_format = CMIT_FMT_DEFAULT; 151 rev.commit_format = CMIT_FMT_DEFAULT;
92 rev.verbose_header = 1; 152 rev.verbose_header = 1;
93 rev.show_root_diff = 0; 153 rev.show_root_diff = 0;
94 setup_revisions(argc, argv, &rev, NULL); 154 setup_revisions(argc, argv, &rev, NULL);
155 load_ref_decorations();
156 rev.show_decorations = 1;
95 rev.grep_filter.regflags |= REG_ICASE; 157 rev.grep_filter.regflags |= REG_ICASE;
96 compile_grep_patterns(&rev.grep_filter); 158 compile_grep_patterns(&rev.grep_filter);
97 prepare_revision_walk(&rev); 159 prepare_revision_walk(&rev);
98 160
99 if (pager) 161 if (pager)
100 html("<table class='list nowrap'>"); 162 html("<table class='list nowrap'>");
101 163
102 html("<tr class='nohover'><th class='left'>Age</th>" 164 html("<tr class='nohover'><th class='left'>Age</th>"
103 "<th class='left'>Commit message</th>" 165 "<th class='left'>Commit message");
104 "<th class='left'>Author</th>"); 166 if (pager) {
167 html(" (");
168 cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL,
169 NULL, ctx.qry.head, ctx.qry.sha1,
170 ctx.qry.path, ctx.qry.ofs, ctx.qry.grep,
171 ctx.qry.search, ctx.qry.showmsg ? 0 : 1);
172 html(")");
173 }
174 html("</th><th class='left'>Author</th>");
105 if (ctx.repo->enable_log_filecount) { 175 if (ctx.repo->enable_log_filecount) {
106 html("<th class='left'>Files</th>"); 176 html("<th class='left'>Files</th>");
107 columns++; 177 columns++;
108 if (ctx.repo->enable_log_linecount) { 178 if (ctx.repo->enable_log_linecount) {
109 html("<th class='left'>Lines</th>"); 179 html("<th class='left'>Lines</th>");
110 columns++; 180 columns++;
111 } 181 }
112 } 182 }
113 html("</tr>\n"); 183 html("</tr>\n");
114 184
115 if (ofs<0) 185 if (ofs<0)
116 ofs = 0; 186 ofs = 0;
@@ -127,29 +197,29 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
127 free(commit->buffer); 197 free(commit->buffer);
128 commit->buffer = NULL; 198 commit->buffer = NULL;
129 free_commit_list(commit->parents); 199 free_commit_list(commit->parents);
130 commit->parents = NULL; 200 commit->parents = NULL;
131 } 201 }
132 if (pager) { 202 if (pager) {
133 htmlf("</table><div class='pager'>", 203 htmlf("</table><div class='pager'>",
134 columns); 204 columns);
135 if (ofs > 0) { 205 if (ofs > 0) {
136 cgit_log_link("[prev]", NULL, NULL, ctx.qry.head, 206 cgit_log_link("[prev]", NULL, NULL, ctx.qry.head,
137 ctx.qry.sha1, ctx.qry.path, 207 ctx.qry.sha1, ctx.qry.path,
138 ofs - cnt, ctx.qry.grep, 208 ofs - cnt, ctx.qry.grep,
139 ctx.qry.search); 209 ctx.qry.search, ctx.qry.showmsg);
140 html("&nbsp;"); 210 html("&nbsp;");
141 } 211 }
142 if ((commit = get_revision(&rev)) != NULL) { 212 if ((commit = get_revision(&rev)) != NULL) {
143 cgit_log_link("[next]", NULL, NULL, ctx.qry.head, 213 cgit_log_link("[next]", NULL, NULL, ctx.qry.head,
144 ctx.qry.sha1, ctx.qry.path, 214 ctx.qry.sha1, ctx.qry.path,
145 ofs + cnt, ctx.qry.grep, 215 ofs + cnt, ctx.qry.grep,
146 ctx.qry.search); 216 ctx.qry.search, ctx.qry.showmsg);
147 } 217 }
148 html("</div>"); 218 html("</div>");
149 } else if ((commit = get_revision(&rev)) != NULL) { 219 } else if ((commit = get_revision(&rev)) != NULL) {
150 html("<tr class='nohover'><td colspan='3'>"); 220 html("<tr class='nohover'><td colspan='3'>");
151 cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, NULL, 0, 221 cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, NULL, 0,
152 NULL, NULL); 222 NULL, NULL, ctx.qry.showmsg);
153 html("</td></tr>\n"); 223 html("</td></tr>\n");
154 } 224 }
155} 225}
diff --git a/ui-patch.c b/ui-patch.c
index e60877d..1d77336 100644
--- a/ui-patch.c
+++ b/ui-patch.c
diff --git a/ui-plain.c b/ui-plain.c
index be559e0..5addd9e 100644
--- a/ui-plain.c
+++ b/ui-plain.c
@@ -9,25 +9,25 @@
9#include "cgit.h" 9#include "cgit.h"
10#include "html.h" 10#include "html.h"
11#include "ui-shared.h" 11#include "ui-shared.h"
12 12
13char *curr_rev; 13char *curr_rev;
14char *match_path; 14char *match_path;
15int match; 15int match;
16 16
17static void print_object(const unsigned char *sha1, const char *path) 17static void print_object(const unsigned char *sha1, const char *path)
18{ 18{
19 enum object_type type; 19 enum object_type type;
20 char *buf; 20 char *buf;
21 size_t size; 21 unsigned long size;
22 22
23 type = sha1_object_info(sha1, &size); 23 type = sha1_object_info(sha1, &size);
24 if (type == OBJ_BAD) { 24 if (type == OBJ_BAD) {
25 html_status(404, "Not found", 0); 25 html_status(404, "Not found", 0);
26 return; 26 return;
27 } 27 }
28 28
29 buf = read_sha1_file(sha1, &type, &size); 29 buf = read_sha1_file(sha1, &type, &size);
30 if (!buf) { 30 if (!buf) {
31 html_status(404, "Not found", 0); 31 html_status(404, "Not found", 0);
32 return; 32 return;
33 } 33 }
diff --git a/ui-refs.c b/ui-refs.c
index 32e0429..25da00a 100644
--- a/ui-refs.c
+++ b/ui-refs.c
@@ -49,78 +49,110 @@ static int cmp_tag_age(const void *a, const void *b)
49 49
50 return cmp_age(r1->tag->tagger_date, r2->tag->tagger_date); 50 return cmp_age(r1->tag->tagger_date, r2->tag->tagger_date);
51} 51}
52 52
53static int print_branch(struct refinfo *ref) 53static int print_branch(struct refinfo *ref)
54{ 54{
55 struct commitinfo *info = ref->commit; 55 struct commitinfo *info = ref->commit;
56 char *name = (char *)ref->refname; 56 char *name = (char *)ref->refname;
57 57
58 if (!info) 58 if (!info)
59 return 1; 59 return 1;
60 html("<tr><td>"); 60 html("<tr><td>");
61 cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL); 61 cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL,
62 ctx.qry.showmsg);
62 html("</td><td>"); 63 html("</td><td>");
63 64
64 if (ref->object->type == OBJ_COMMIT) { 65 if (ref->object->type == OBJ_COMMIT) {
65 cgit_commit_link(info->subject, NULL, NULL, name, NULL); 66 cgit_commit_link(info->subject, NULL, NULL, name, NULL);
66 html("</td><td>"); 67 html("</td><td>");
67 html_txt(info->author); 68 html_txt(info->author);
68 html("</td><td colspan='2'>"); 69 html("</td><td colspan='2'>");
69 cgit_print_age(info->commit->date, -1, NULL); 70 cgit_print_age(info->commit->date, -1, NULL);
70 } else { 71 } else {
71 html("</td><td></td><td>"); 72 html("</td><td></td><td>");
72 cgit_object_link(ref->object); 73 cgit_object_link(ref->object);
73 } 74 }
74 html("</td></tr>\n"); 75 html("</td></tr>\n");
75 return 0; 76 return 0;
76} 77}
77 78
78static void print_tag_header() 79static void print_tag_header()
79{ 80{
80 html("<tr class='nohover'><th class='left'>Tag</th>" 81 html("<tr class='nohover'><th class='left'>Tag</th>"
81 "<th class='left'>Reference</th>" 82 "<th class='left'>Download</th>"
82 "<th class='left'>Author</th>" 83 "<th class='left'>Author</th>"
83 "<th class='left' colspan='2'>Age</th></tr>\n"); 84 "<th class='left' colspan='2'>Age</th></tr>\n");
84 header = 1; 85 header = 1;
85} 86}
86 87
88static void print_tag_downloads(const struct cgit_repo *repo, const char *ref)
89{
90 const struct cgit_snapshot_format* f;
91 char *filename;
92 const char *basename;
93
94 if (!ref || strlen(ref) < 2)
95 return;
96
97 basename = cgit_repobasename(repo->url);
98 if (prefixcmp(ref, basename) != 0) {
99 if ((ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]))
100 ref++;
101 if (isdigit(ref[0]))
102 ref = xstrdup(fmt("%s-%s", basename, ref));
103 }
104
105 for (f = cgit_snapshot_formats; f->suffix; f++) {
106 if (!(repo->snapshots & f->bit))
107 continue;
108 filename = fmt("%s%s", ref, f->suffix);
109 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename);
110 html("&nbsp;&nbsp;");
111 }
112}
87static int print_tag(struct refinfo *ref) 113static int print_tag(struct refinfo *ref)
88{ 114{
89 struct tag *tag; 115 struct tag *tag;
90 struct taginfo *info; 116 struct taginfo *info;
91 char *name = (char *)ref->refname; 117 char *name = (char *)ref->refname;
92 118
93 if (ref->object->type == OBJ_TAG) { 119 if (ref->object->type == OBJ_TAG) {
94 tag = (struct tag *)ref->object; 120 tag = (struct tag *)ref->object;
95 info = ref->tag; 121 info = ref->tag;
96 if (!tag || !info) 122 if (!tag || !info)
97 return 1; 123 return 1;
98 html("<tr><td>"); 124 html("<tr><td>");
99 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name); 125 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name);
100 html("</td><td>"); 126 html("</td><td>");
127 if (ctx.repo->snapshots && (tag->tagged->type == OBJ_COMMIT))
128 print_tag_downloads(ctx.repo, name);
129 else
101 cgit_object_link(tag->tagged); 130 cgit_object_link(tag->tagged);
102 html("</td><td>"); 131 html("</td><td>");
103 if (info->tagger) 132 if (info->tagger)
104 html(info->tagger); 133 html(info->tagger);
105 html("</td><td colspan='2'>"); 134 html("</td><td colspan='2'>");
106 if (info->tagger_date > 0) 135 if (info->tagger_date > 0)
107 cgit_print_age(info->tagger_date, -1, NULL); 136 cgit_print_age(info->tagger_date, -1, NULL);
108 html("</td></tr>\n"); 137 html("</td></tr>\n");
109 } else { 138 } else {
110 if (!header) 139 if (!header)
111 print_tag_header(); 140 print_tag_header();
112 html("<tr><td>"); 141 html("<tr><td>");
113 html_txt(name); 142 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name);
114 html("</td><td>"); 143 html("</td><td>");
144 if (ctx.repo->snapshots && (ref->object->type == OBJ_COMMIT))
145 print_tag_downloads(ctx.repo, name);
146 else
115 cgit_object_link(ref->object); 147 cgit_object_link(ref->object);
116 html("</td></tr>\n"); 148 html("</td></tr>\n");
117 } 149 }
118 return 0; 150 return 0;
119} 151}
120 152
121static void print_refs_link(char *path) 153static void print_refs_link(char *path)
122{ 154{
123 html("<tr class='nohover'><td colspan='4'>"); 155 html("<tr class='nohover'><td colspan='4'>");
124 cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path); 156 cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path);
125 html("</td></tr>"); 157 html("</td></tr>");
126} 158}
diff --git a/ui-repolist.c b/ui-repolist.c
index ab050c7..2c13d50 100644
--- a/ui-repolist.c
+++ b/ui-repolist.c
@@ -1,56 +1,78 @@
1/* ui-repolist.c: functions for generating the repolist page 1/* ui-repolist.c: functions for generating the repolist page
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/* This is needed for strcasestr to be defined by <string.h> */
10#define _GNU_SOURCE 1
11#include <string.h>
12
9#include <time.h> 13#include <time.h>
10 14
11#include "cgit.h" 15#include "cgit.h"
12#include "html.h" 16#include "html.h"
13#include "ui-shared.h" 17#include "ui-shared.h"
14 18
15time_t read_agefile(char *path) 19time_t read_agefile(char *path)
16{ 20{
17 FILE *f; 21 FILE *f;
18 static char buf[64], buf2[64]; 22 static char buf[64], buf2[64];
19 23
20 if (!(f = fopen(path, "r"))) 24 if (!(f = fopen(path, "r")))
21 return -1; 25 return -1;
22 fgets(buf, sizeof(buf), f); 26 if (fgets(buf, sizeof(buf), f) == NULL)
27 return -1;
23 fclose(f); 28 fclose(f);
24 if (parse_date(buf, buf2, sizeof(buf2))) 29 if (parse_date(buf, buf2, sizeof(buf2)))
25 return strtoul(buf2, NULL, 10); 30 return strtoul(buf2, NULL, 10);
26 else 31 else
27 return 0; 32 return 0;
28} 33}
29 34
30static void print_modtime(struct cgit_repo *repo) 35static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime)
31{ 36{
32 char *path; 37 char *path;
33 struct stat s; 38 struct stat s;
39 struct cgit_repo *r = (struct cgit_repo *)repo;
34 40
41 if (repo->mtime != -1) {
42 *mtime = repo->mtime;
43 return 1;
44 }
35 path = fmt("%s/%s", repo->path, ctx.cfg.agefile); 45 path = fmt("%s/%s", repo->path, ctx.cfg.agefile);
36 if (stat(path, &s) == 0) { 46 if (stat(path, &s) == 0) {
37 cgit_print_age(read_agefile(path), -1, NULL); 47 *mtime = read_agefile(path);
38 return; 48 r->mtime = *mtime;
49 return 1;
39 } 50 }
40 51
41 path = fmt("%s/refs/heads/%s", repo->path, repo->defbranch); 52 path = fmt("%s/refs/heads/%s", repo->path, repo->defbranch);
42 if (stat(path, &s) != 0) 53 if (stat(path, &s) == 0)
43 return; 54 *mtime = s.st_mtime;
44 cgit_print_age(s.st_mtime, -1, NULL); 55 else
56 *mtime = 0;
57
58 r->mtime = *mtime;
59 return (r->mtime != 0);
60}
61
62static void print_modtime(struct cgit_repo *repo)
63{
64 time_t t;
65 if (get_repo_modtime(repo, &t))
66 cgit_print_age(t, -1, NULL);
45} 67}
46 68
47int is_match(struct cgit_repo *repo) 69int is_match(struct cgit_repo *repo)
48{ 70{
49 if (!ctx.qry.search) 71 if (!ctx.qry.search)
50 return 1; 72 return 1;
51 if (repo->url && strcasestr(repo->url, ctx.qry.search)) 73 if (repo->url && strcasestr(repo->url, ctx.qry.search))
52 return 1; 74 return 1;
53 if (repo->name && strcasestr(repo->name, ctx.qry.search)) 75 if (repo->name && strcasestr(repo->name, ctx.qry.search))
54 return 1; 76 return 1;
55 if (repo->desc && strcasestr(repo->desc, ctx.qry.search)) 77 if (repo->desc && strcasestr(repo->desc, ctx.qry.search))
56 return 1; 78 return 1;
@@ -59,102 +81,192 @@ int is_match(struct cgit_repo *repo)
59 return 0; 81 return 0;
60} 82}
61 83
62int is_in_url(struct cgit_repo *repo) 84int is_in_url(struct cgit_repo *repo)
63{ 85{
64 if (!ctx.qry.url) 86 if (!ctx.qry.url)
65 return 1; 87 return 1;
66 if (repo->url && !prefixcmp(repo->url, ctx.qry.url)) 88 if (repo->url && !prefixcmp(repo->url, ctx.qry.url))
67 return 1; 89 return 1;
68 return 0; 90 return 0;
69} 91}
70 92
93void print_sort_header(const char *title, const char *sort)
94{
95 htmlf("<th class='left'><a href='./?s=%s", sort);
96 if (ctx.qry.search) {
97 html("&q=");
98 html_url_arg(ctx.qry.search);
99 }
100 htmlf("'>%s</a></th>", title);
101}
102
71void print_header(int columns) 103void print_header(int columns)
72{ 104{
73 html("<tr class='nohover'>" 105 html("<tr class='nohover'>");
74 "<th class='left'>Name</th>" 106 print_sort_header("Name", "name");
75 "<th class='left'>Description</th>" 107 print_sort_header("Description", "desc");
76 "<th class='left'>Owner</th>" 108 print_sort_header("Owner", "owner");
77 "<th class='left'>Idle</th>"); 109 print_sort_header("Idle", "idle");
78 if (ctx.cfg.enable_index_links) 110 if (ctx.cfg.enable_index_links)
79 html("<th class='left'>Links</th>"); 111 html("<th class='left'>Links</th>");
80 html("</tr>\n"); 112 html("</tr>\n");
81} 113}
82 114
83 115
84void print_pager(int items, int pagelen, char *search) 116void print_pager(int items, int pagelen, char *search)
85{ 117{
86 int i; 118 int i;
87 html("<div class='pager'>"); 119 html("<div class='pager'>");
88 for(i = 0; i * pagelen < items; i++) 120 for(i = 0; i * pagelen < items; i++)
89 cgit_index_link(fmt("[%d]", i+1), fmt("Page %d", i+1), NULL, 121 cgit_index_link(fmt("[%d]", i+1), fmt("Page %d", i+1), NULL,
90 search, i * pagelen); 122 search, i * pagelen);
91 html("</div>"); 123 html("</div>");
92} 124}
93 125
126static int cmp(const char *s1, const char *s2)
127{
128 if (s1 && s2)
129 return strcmp(s1, s2);
130 if (s1 && !s2)
131 return -1;
132 if (s2 && !s1)
133 return 1;
134 return 0;
135}
136
137static int sort_name(const void *a, const void *b)
138{
139 const struct cgit_repo *r1 = a;
140 const struct cgit_repo *r2 = b;
141
142 return cmp(r1->name, r2->name);
143}
144
145static int sort_desc(const void *a, const void *b)
146{
147 const struct cgit_repo *r1 = a;
148 const struct cgit_repo *r2 = b;
149
150 return cmp(r1->desc, r2->desc);
151}
152
153static int sort_owner(const void *a, const void *b)
154{
155 const struct cgit_repo *r1 = a;
156 const struct cgit_repo *r2 = b;
157
158 return cmp(r1->owner, r2->owner);
159}
160
161static int sort_idle(const void *a, const void *b)
162{
163 const struct cgit_repo *r1 = a;
164 const struct cgit_repo *r2 = b;
165 time_t t1, t2;
166
167 t1 = t2 = 0;
168 get_repo_modtime(r1, &t1);
169 get_repo_modtime(r2, &t2);
170 return t2 - t1;
171}
172
173struct sortcolumn {
174 const char *name;
175 int (*fn)(const void *a, const void *b);
176};
177
178struct sortcolumn sortcolumn[] = {
179 {"name", sort_name},
180 {"desc", sort_desc},
181 {"owner", sort_owner},
182 {"idle", sort_idle},
183 {NULL, NULL}
184};
185
186int sort_repolist(char *field)
187{
188 struct sortcolumn *column;
189
190 for (column = &sortcolumn[0]; column->name; column++) {
191 if (strcmp(field, column->name))
192 continue;
193 qsort(cgit_repolist.repos, cgit_repolist.count,
194 sizeof(struct cgit_repo), column->fn);
195 return 1;
196 }
197 return 0;
198}
199
200
94void cgit_print_repolist() 201void cgit_print_repolist()
95{ 202{
96 int i, columns = 4, hits = 0, header = 0; 203 int i, columns = 4, hits = 0, header = 0;
97 char *last_group = NULL; 204 char *last_group = NULL;
205 int sorted = 0;
98 206
99 if (ctx.cfg.enable_index_links) 207 if (ctx.cfg.enable_index_links)
100 columns++; 208 columns++;
101 209
102 ctx.page.title = ctx.cfg.root_title; 210 ctx.page.title = ctx.cfg.root_title;
103 cgit_print_http_headers(&ctx); 211 cgit_print_http_headers(&ctx);
104 cgit_print_docstart(&ctx); 212 cgit_print_docstart(&ctx);
105 cgit_print_pageheader(&ctx); 213 cgit_print_pageheader(&ctx);
106 214
107 if (ctx.cfg.index_header) 215 if (ctx.cfg.index_header)
108 html_include(ctx.cfg.index_header); 216 html_include(ctx.cfg.index_header);
109 217
218 if(ctx.qry.sort)
219 sorted = sort_repolist(ctx.qry.sort);
220
110 html("<table summary='repository list' class='list nowrap'>"); 221 html("<table summary='repository list' class='list nowrap'>");
111 for (i=0; i<cgit_repolist.count; i++) { 222 for (i=0; i<cgit_repolist.count; i++) {
112 ctx.repo = &cgit_repolist.repos[i]; 223 ctx.repo = &cgit_repolist.repos[i];
113 if (!(is_match(ctx.repo) && is_in_url(ctx.repo))) 224 if (!(is_match(ctx.repo) && is_in_url(ctx.repo)))
114 continue; 225 continue;
115 hits++; 226 hits++;
116 if (hits <= ctx.qry.ofs) 227 if (hits <= ctx.qry.ofs)
117 continue; 228 continue;
118 if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count) 229 if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count)
119 continue; 230 continue;
120 if (!header++) 231 if (!header++)
121 print_header(columns); 232 print_header(columns);
122 if ((last_group == NULL && ctx.repo->group != NULL) || 233 if (!sorted &&
234 ((last_group == NULL && ctx.repo->group != NULL) ||
123 (last_group != NULL && ctx.repo->group == NULL) || 235 (last_group != NULL && ctx.repo->group == NULL) ||
124 (last_group != NULL && ctx.repo->group != NULL && 236 (last_group != NULL && ctx.repo->group != NULL &&
125 strcmp(ctx.repo->group, last_group))) { 237 strcmp(ctx.repo->group, last_group)))) {
126 htmlf("<tr class='nohover'><td colspan='%d' class='repogroup'>", 238 htmlf("<tr class='nohover'><td colspan='%d' class='repogroup'>",
127 columns); 239 columns);
128 html_txt(ctx.repo->group); 240 html_txt(ctx.repo->group);
129 html("</td></tr>"); 241 html("</td></tr>");
130 last_group = ctx.repo->group; 242 last_group = ctx.repo->group;
131 } 243 }
132 htmlf("<tr><td class='%s'>", 244 htmlf("<tr><td class='%s'>",
133 ctx.repo->group ? "sublevel-repo" : "toplevel-repo"); 245 !sorted && ctx.repo->group ? "sublevel-repo" : "toplevel-repo");
134 cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL); 246 cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
135 html("</td><td>"); 247 html("</td><td>");
136 html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL); 248 html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL);
137 html_ntxt(ctx.cfg.max_repodesc_len, ctx.repo->desc); 249 html_ntxt(ctx.cfg.max_repodesc_len, ctx.repo->desc);
138 html_link_close(); 250 html_link_close();
139 html("</td><td>"); 251 html("</td><td>");
140 html_txt(ctx.repo->owner); 252 html_txt(ctx.repo->owner);
141 html("</td><td>"); 253 html("</td><td>");
142 print_modtime(ctx.repo); 254 print_modtime(ctx.repo);
143 html("</td>"); 255 html("</td>");
144 if (ctx.cfg.enable_index_links) { 256 if (ctx.cfg.enable_index_links) {
145 html("<td>"); 257 html("<td>");
146 cgit_summary_link("summary", NULL, "button", NULL); 258 cgit_summary_link("summary", NULL, "button", NULL);
147 cgit_log_link("log", NULL, "button", NULL, NULL, NULL, 259 cgit_log_link("log", NULL, "button", NULL, NULL, NULL,
148 0, NULL, NULL); 260 0, NULL, NULL, ctx.qry.showmsg);
149 cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL); 261 cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL);
150 html("</td>"); 262 html("</td>");
151 } 263 }
152 html("</tr>\n"); 264 html("</tr>\n");
153 } 265 }
154 html("</table>"); 266 html("</table>");
155 if (!hits) 267 if (!hits)
156 cgit_print_error("No repositories found"); 268 cgit_print_error("No repositories found");
157 else if (hits > ctx.cfg.max_repo_count) 269 else if (hits > ctx.cfg.max_repo_count)
158 print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search); 270 print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search);
159 cgit_print_docend(); 271 cgit_print_docend();
160} 272}
diff --git a/ui-shared.c b/ui-shared.c
index 1bb30c2..4f28512 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -272,48 +272,54 @@ void cgit_tree_link(char *name, char *title, char *class, char *head,
272 char *rev, char *path) 272 char *rev, char *path)
273{ 273{
274 reporevlink("tree", name, title, class, head, rev, path); 274 reporevlink("tree", name, title, class, head, rev, path);
275} 275}
276 276
277void cgit_plain_link(char *name, char *title, char *class, char *head, 277void cgit_plain_link(char *name, char *title, char *class, char *head,
278 char *rev, char *path) 278 char *rev, char *path)
279{ 279{
280 reporevlink("plain", name, title, class, head, rev, path); 280 reporevlink("plain", name, title, class, head, rev, path);
281} 281}
282 282
283void cgit_log_link(char *name, char *title, char *class, char *head, 283void cgit_log_link(char *name, char *title, char *class, char *head,
284 char *rev, char *path, int ofs, char *grep, char *pattern) 284 char *rev, char *path, int ofs, char *grep, char *pattern,
285 int showmsg)
285{ 286{
286 char *delim; 287 char *delim;
287 288
288 delim = repolink(title, class, "log", head, path); 289 delim = repolink(title, class, "log", head, path);
289 if (rev && strcmp(rev, ctx.qry.head)) { 290 if (rev && strcmp(rev, ctx.qry.head)) {
290 html(delim); 291 html(delim);
291 html("id="); 292 html("id=");
292 html_url_arg(rev); 293 html_url_arg(rev);
293 delim = "&"; 294 delim = "&";
294 } 295 }
295 if (grep && pattern) { 296 if (grep && pattern) {
296 html(delim); 297 html(delim);
297 html("qt="); 298 html("qt=");
298 html_url_arg(grep); 299 html_url_arg(grep);
299 delim = "&"; 300 delim = "&";
300 html(delim); 301 html(delim);
301 html("q="); 302 html("q=");
302 html_url_arg(pattern); 303 html_url_arg(pattern);
303 } 304 }
304 if (ofs > 0) { 305 if (ofs > 0) {
305 html(delim); 306 html(delim);
306 html("ofs="); 307 html("ofs=");
307 htmlf("%d", ofs); 308 htmlf("%d", ofs);
309 delim = "&";
310 }
311 if (showmsg) {
312 html(delim);
313 html("showmsg=1");
308 } 314 }
309 html("'>"); 315 html("'>");
310 html_txt(name); 316 html_txt(name);
311 html("</a>"); 317 html("</a>");
312} 318}
313 319
314void cgit_commit_link(char *name, char *title, char *class, char *head, 320void cgit_commit_link(char *name, char *title, char *class, char *head,
315 char *rev) 321 char *rev)
316{ 322{
317 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) { 323 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
318 name[ctx.cfg.max_msg_len] = '\0'; 324 name[ctx.cfg.max_msg_len] = '\0';
319 name[ctx.cfg.max_msg_len - 1] = '.'; 325 name[ctx.cfg.max_msg_len - 1] = '.';
@@ -362,39 +368,41 @@ void cgit_patch_link(char *name, char *title, char *class, char *head,
362{ 368{
363 reporevlink("patch", name, title, class, head, rev, NULL); 369 reporevlink("patch", name, title, class, head, rev, NULL);
364} 370}
365 371
366void cgit_stats_link(char *name, char *title, char *class, char *head, 372void cgit_stats_link(char *name, char *title, char *class, char *head,
367 char *path) 373 char *path)
368{ 374{
369 reporevlink("stats", name, title, class, head, NULL, path); 375 reporevlink("stats", name, title, class, head, NULL, path);
370} 376}
371 377
372void cgit_object_link(struct object *obj) 378void cgit_object_link(struct object *obj)
373{ 379{
374 char *page, *rev, *name; 380 char *page, *shortrev, *fullrev, *name;
375 381
382 fullrev = sha1_to_hex(obj->sha1);
383 shortrev = xstrdup(fullrev);
384 shortrev[10] = '\0';
376 if (obj->type == OBJ_COMMIT) { 385 if (obj->type == OBJ_COMMIT) {
377 cgit_commit_link(fmt("commit %s", sha1_to_hex(obj->sha1)), NULL, NULL, 386 cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
378 ctx.qry.head, sha1_to_hex(obj->sha1)); 387 ctx.qry.head, fullrev);
379 return; 388 return;
380 } else if (obj->type == OBJ_TREE) 389 } else if (obj->type == OBJ_TREE)
381 page = "tree"; 390 page = "tree";
382 else if (obj->type == OBJ_TAG) 391 else if (obj->type == OBJ_TAG)
383 page = "tag"; 392 page = "tag";
384 else 393 else
385 page = "blob"; 394 page = "blob";
386 rev = sha1_to_hex(obj->sha1); 395 name = fmt("%s %s...", typename(obj->type), shortrev);
387 name = fmt("%s %s", typename(obj->type), rev); 396 reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
388 reporevlink(page, name, NULL, NULL, ctx.qry.head, rev, NULL);
389} 397}
390 398
391void cgit_print_date(time_t secs, char *format, int local_time) 399void cgit_print_date(time_t secs, char *format, int local_time)
392{ 400{
393 char buf[64]; 401 char buf[64];
394 struct tm *time; 402 struct tm *time;
395 403
396 if (!secs) 404 if (!secs)
397 return; 405 return;
398 if(local_time) 406 if(local_time)
399 time = localtime(&secs); 407 time = localtime(&secs);
400 else 408 else
@@ -565,42 +573,49 @@ void cgit_add_hidden_formfields(int incl_head, int incl_search, char *page)
565 url = fmt("%s/%s", url, ctx.qry.path); 573 url = fmt("%s/%s", url, ctx.qry.path);
566 html_hidden("url", url); 574 html_hidden("url", url);
567 } 575 }
568 576
569 if (incl_head && ctx.qry.head && ctx.repo->defbranch && 577 if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
570 strcmp(ctx.qry.head, ctx.repo->defbranch)) 578 strcmp(ctx.qry.head, ctx.repo->defbranch))
571 html_hidden("h", ctx.qry.head); 579 html_hidden("h", ctx.qry.head);
572 580
573 if (ctx.qry.sha1) 581 if (ctx.qry.sha1)
574 html_hidden("id", ctx.qry.sha1); 582 html_hidden("id", ctx.qry.sha1);
575 if (ctx.qry.sha2) 583 if (ctx.qry.sha2)
576 html_hidden("id2", ctx.qry.sha2); 584 html_hidden("id2", ctx.qry.sha2);
585 if (ctx.qry.showmsg)
586 html_hidden("showmsg", "1");
577 587
578 if (incl_search) { 588 if (incl_search) {
579 if (ctx.qry.grep) 589 if (ctx.qry.grep)
580 html_hidden("qt", ctx.qry.grep); 590 html_hidden("qt", ctx.qry.grep);
581 if (ctx.qry.search) 591 if (ctx.qry.search)
582 html_hidden("q", ctx.qry.search); 592 html_hidden("q", ctx.qry.search);
583 } 593 }
584} 594}
585 595
596const char *fallback_cmd = "repolist";
597
586char *hc(struct cgit_cmd *cmd, const char *page) 598char *hc(struct cgit_cmd *cmd, const char *page)
587{ 599{
588 return (strcmp(cmd->name, page) ? NULL : "active"); 600 return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active");
589} 601}
590 602
591void cgit_print_pageheader(struct cgit_context *ctx) 603void cgit_print_pageheader(struct cgit_context *ctx)
592{ 604{
593 struct cgit_cmd *cmd = cgit_get_cmd(ctx); 605 struct cgit_cmd *cmd = cgit_get_cmd(ctx);
594 606
607 if (!cmd && ctx->repo)
608 fallback_cmd = "summary";
609
595 html("<table id='header'>\n"); 610 html("<table id='header'>\n");
596 html("<tr>\n"); 611 html("<tr>\n");
597 html("<td class='logo' rowspan='2'><a href='"); 612 html("<td class='logo' rowspan='2'><a href='");
598 if (ctx->cfg.logo_link) 613 if (ctx->cfg.logo_link)
599 html_attr(ctx->cfg.logo_link); 614 html_attr(ctx->cfg.logo_link);
600 else 615 else
601 html_attr(cgit_rooturl()); 616 html_attr(cgit_rooturl());
602 html("'><img src='"); 617 html("'><img src='");
603 html_attr(ctx->cfg.logo); 618 html_attr(ctx->cfg.logo);
604 html("' alt='cgit logo'/></a></td>\n"); 619 html("' alt='cgit logo'/></a></td>\n");
605 620
606 html("<td class='main'>"); 621 html("<td class='main'>");
@@ -631,25 +646,25 @@ void cgit_print_pageheader(struct cgit_context *ctx)
631 else if (ctx->cfg.index_info) 646 else if (ctx->cfg.index_info)
632 html_include(ctx->cfg.index_info); 647 html_include(ctx->cfg.index_info);
633 } 648 }
634 html("</td></tr></table>\n"); 649 html("</td></tr></table>\n");
635 650
636 html("<table class='tabs'><tr><td>\n"); 651 html("<table class='tabs'><tr><td>\n");
637 if (ctx->repo) { 652 if (ctx->repo) {
638 cgit_summary_link("summary", NULL, hc(cmd, "summary"), 653 cgit_summary_link("summary", NULL, hc(cmd, "summary"),
639 ctx->qry.head); 654 ctx->qry.head);
640 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head, 655 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head,
641 ctx->qry.sha1, NULL); 656 ctx->qry.sha1, NULL);
642 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head, 657 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head,
643 NULL, NULL, 0, NULL, NULL); 658 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg);
644 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 659 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
645 ctx->qry.sha1, NULL); 660 ctx->qry.sha1, NULL);
646 cgit_commit_link("commit", NULL, hc(cmd, "commit"), 661 cgit_commit_link("commit", NULL, hc(cmd, "commit"),
647 ctx->qry.head, ctx->qry.sha1); 662 ctx->qry.head, ctx->qry.sha1);
648 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 663 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
649 ctx->qry.sha1, ctx->qry.sha2, NULL); 664 ctx->qry.sha1, ctx->qry.sha2, NULL);
650 if (ctx->repo->max_stats) 665 if (ctx->repo->max_stats)
651 cgit_stats_link("stats", NULL, hc(cmd, "stats"), 666 cgit_stats_link("stats", NULL, hc(cmd, "stats"),
652 ctx->qry.head, NULL); 667 ctx->qry.head, NULL);
653 if (ctx->repo->readme) 668 if (ctx->repo->readme)
654 reporevlink("about", "about", NULL, 669 reporevlink("about", "about", NULL,
655 hc(cmd, "about"), ctx->qry.head, NULL, 670 hc(cmd, "about"), ctx->qry.head, NULL,
@@ -707,17 +722,16 @@ void cgit_print_filemode(unsigned short mode)
707 722
708void cgit_print_snapshot_links(const char *repo, const char *head, 723void cgit_print_snapshot_links(const char *repo, const char *head,
709 const char *hex, int snapshots) 724 const char *hex, int snapshots)
710{ 725{
711 const struct cgit_snapshot_format* f; 726 const struct cgit_snapshot_format* f;
712 char *filename; 727 char *filename;
713 728
714 for (f = cgit_snapshot_formats; f->suffix; f++) { 729 for (f = cgit_snapshot_formats; f->suffix; f++) {
715 if (!(snapshots & f->bit)) 730 if (!(snapshots & f->bit))
716 continue; 731 continue;
717 filename = fmt("%s-%s%s", cgit_repobasename(repo), hex, 732 filename = fmt("%s-%s%s", cgit_repobasename(repo), hex,
718 f->suffix); 733 f->suffix);
719 cgit_snapshot_link(filename, NULL, NULL, (char *)head, 734 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename);
720 (char *)hex, filename);
721 html("<br/>"); 735 html("<br/>");
722 } 736 }
723} 737}
diff --git a/ui-shared.h b/ui-shared.h
index 1524dc3..5a3821f 100644
--- a/ui-shared.h
+++ b/ui-shared.h
@@ -10,25 +10,25 @@ extern char *cgit_pageurl(const char *reponame, const char *pagename,
10 10
11extern void cgit_index_link(char *name, char *title, char *class, 11extern void cgit_index_link(char *name, char *title, char *class,
12 char *pattern, int ofs); 12 char *pattern, int ofs);
13extern void cgit_summary_link(char *name, char *title, char *class, char *head); 13extern void cgit_summary_link(char *name, char *title, char *class, char *head);
14extern void cgit_tag_link(char *name, char *title, char *class, char *head, 14extern void cgit_tag_link(char *name, char *title, char *class, char *head,
15 char *rev); 15 char *rev);
16extern void cgit_tree_link(char *name, char *title, char *class, char *head, 16extern void cgit_tree_link(char *name, char *title, char *class, char *head,
17 char *rev, char *path); 17 char *rev, char *path);
18extern void cgit_plain_link(char *name, char *title, char *class, char *head, 18extern void cgit_plain_link(char *name, char *title, char *class, char *head,
19 char *rev, char *path); 19 char *rev, char *path);
20extern void cgit_log_link(char *name, char *title, char *class, char *head, 20extern void cgit_log_link(char *name, char *title, char *class, char *head,
21 char *rev, char *path, int ofs, char *grep, 21 char *rev, char *path, int ofs, char *grep,
22 char *pattern); 22 char *pattern, int showmsg);
23extern void cgit_commit_link(char *name, char *title, char *class, char *head, 23extern void cgit_commit_link(char *name, char *title, char *class, char *head,
24 char *rev); 24 char *rev);
25extern void cgit_patch_link(char *name, char *title, char *class, char *head, 25extern void cgit_patch_link(char *name, char *title, char *class, char *head,
26 char *rev); 26 char *rev);
27extern void cgit_refs_link(char *name, char *title, char *class, char *head, 27extern void cgit_refs_link(char *name, char *title, char *class, char *head,
28 char *rev, char *path); 28 char *rev, char *path);
29extern void cgit_snapshot_link(char *name, char *title, char *class, 29extern void cgit_snapshot_link(char *name, char *title, char *class,
30 char *head, char *rev, char *archivename); 30 char *head, char *rev, char *archivename);
31extern void cgit_diff_link(char *name, char *title, char *class, char *head, 31extern void cgit_diff_link(char *name, char *title, char *class, char *head,
32 char *new_rev, char *old_rev, char *path); 32 char *new_rev, char *old_rev, char *path);
33extern void cgit_stats_link(char *name, char *title, char *class, char *head, 33extern void cgit_stats_link(char *name, char *title, char *class, char *head,
34 char *path); 34 char *path);
diff --git a/ui-snapshot.c b/ui-snapshot.c
index 9c4d086..f25613e 100644
--- a/ui-snapshot.c
+++ b/ui-snapshot.c
@@ -49,26 +49,26 @@ static int write_compressed_tar_archive(struct archiver_args *args,const char *f
49static int write_tar_gzip_archive(struct archiver_args *args) 49static int write_tar_gzip_archive(struct archiver_args *args)
50{ 50{
51 return write_compressed_tar_archive(args,"gzip"); 51 return write_compressed_tar_archive(args,"gzip");
52} 52}
53 53
54static int write_tar_bzip2_archive(struct archiver_args *args) 54static int write_tar_bzip2_archive(struct archiver_args *args)
55{ 55{
56 return write_compressed_tar_archive(args,"bzip2"); 56 return write_compressed_tar_archive(args,"bzip2");
57} 57}
58 58
59const struct cgit_snapshot_format cgit_snapshot_formats[] = { 59const struct cgit_snapshot_format cgit_snapshot_formats[] = {
60 { ".zip", "application/x-zip", write_zip_archive, 0x1 }, 60 { ".zip", "application/x-zip", write_zip_archive, 0x1 },
61 { ".tar.gz", "application/x-tar", write_tar_gzip_archive, 0x2 }, 61 { ".tar.gz", "application/x-gzip", write_tar_gzip_archive, 0x2 },
62 { ".tar.bz2", "application/x-tar", write_tar_bzip2_archive, 0x4 }, 62 { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive, 0x4 },
63 { ".tar", "application/x-tar", write_tar_archive, 0x8 }, 63 { ".tar", "application/x-tar", write_tar_archive, 0x8 },
64 {} 64 {}
65}; 65};
66 66
67static const struct cgit_snapshot_format *get_format(const char *filename) 67static const struct cgit_snapshot_format *get_format(const char *filename)
68{ 68{
69 const struct cgit_snapshot_format *fmt; 69 const struct cgit_snapshot_format *fmt;
70 int fl, sl; 70 int fl, sl;
71 71
72 fl = strlen(filename); 72 fl = strlen(filename);
73 for(fmt = cgit_snapshot_formats; fmt->suffix; fmt++) { 73 for(fmt = cgit_snapshot_formats; fmt->suffix; fmt++) {
74 sl = strlen(fmt->suffix); 74 sl = strlen(fmt->suffix);
@@ -105,84 +105,90 @@ static int make_snapshot(const struct cgit_snapshot_format *format,
105 args.base = ""; 105 args.base = "";
106 args.baselen = 0; 106 args.baselen = 0;
107 } 107 }
108 args.tree = commit->tree; 108 args.tree = commit->tree;
109 args.time = commit->date; 109 args.time = commit->date;
110 ctx.page.mimetype = xstrdup(format->mimetype); 110 ctx.page.mimetype = xstrdup(format->mimetype);
111 ctx.page.filename = xstrdup(filename); 111 ctx.page.filename = xstrdup(filename);
112 cgit_print_http_headers(&ctx); 112 cgit_print_http_headers(&ctx);
113 format->write_func(&args); 113 format->write_func(&args);
114 return 0; 114 return 0;
115} 115}
116 116
117char *dwim_filename = NULL; 117/* Try to guess the requested revision from the requested snapshot name.
118const char *dwim_refname = NULL; 118 * First the format extension is stripped, e.g. "cgit-0.7.2.tar.gz" become
119 119 * "cgit-0.7.2". If this is a valid commit object name we've got a winner.
120static int ref_cb(const char *refname, const unsigned char *sha1, int flags, 120 * Otherwise, if the snapshot name has a prefix matching the result from
121 void *cb_data) 121 * repo_basename(), we strip the basename and any following '-' and '_'
122{ 122 * characters ("cgit-0.7.2" -> "0.7.2") and check the resulting name once
123 const char *r = refname; 123 * more. If this still isn't a valid commit object name, we check if pre-
124 while (r && *r) { 124 * pending a 'v' to the remaining snapshot name ("0.7.2" -> "v0.7.2") gives
125 fprintf(stderr, " cmp %s with %s:", dwim_filename, r); 125 * us something valid.
126 if (!strcmp(dwim_filename, r)) {
127 fprintf(stderr, "MATCH!\n");
128 dwim_refname = refname;
129 return 1;
130 }
131 fprintf(stderr, "no match\n");
132 if (isdigit(*r))
133 break;
134 r++;
135 }
136 return 0;
137}
138
139/* Try to guess the requested revision by combining repo name and tag name
140 * and comparing this to the requested snapshot name. E.g. the requested
141 * snapshot is "cgit-0.7.2.tar.gz" while repo name is "cgit" and tag name
142 * is "v0.7.2". First, the reponame is stripped off, leaving "-0.7.2.tar.gz".
143 * Next, any '-' and '_' characters are stripped, leaving "0.7.2.tar.gz".
144 * Finally, the requested format suffix is removed and we end up with "0.7.2".
145 * Then we test each tag against this dwimmed filename, and for each tag
146 * we even try to remove any leading characters which are non-digits. I.e.
147 * we first compare with "v0.7.2", then with "0.7.2" and we've got a match.
148 */ 126 */
149static const char *get_ref_from_filename(const char *url, const char *filename, 127static const char *get_ref_from_filename(const char *url, const char *filename,
150 const struct cgit_snapshot_format *fmt) 128 const struct cgit_snapshot_format *format)
151{ 129{
152 const char *reponame = cgit_repobasename(url); 130 const char *reponame;
153 fprintf(stderr, "reponame=%s, filename=%s\n", reponame, filename); 131 unsigned char sha1[20];
154 if (prefixcmp(filename, reponame)) 132 char *snapshot;
133
134 snapshot = xstrdup(filename);
135 snapshot[strlen(snapshot) - strlen(format->suffix)] = '\0';
136 fprintf(stderr, "snapshot=%s\n", snapshot);
137
138 if (get_sha1(snapshot, sha1) == 0)
139 return snapshot;
140
141 reponame = cgit_repobasename(url);
142 fprintf(stderr, "reponame=%s\n", reponame);
143 if (prefixcmp(snapshot, reponame) == 0) {
144 snapshot += strlen(reponame);
145 while (snapshot && (*snapshot == '-' || *snapshot == '_'))
146 snapshot++;
147 }
148
149 if (get_sha1(snapshot, sha1) == 0)
150 return snapshot;
151
152 snapshot = fmt("v%s", snapshot);
153 if (get_sha1(snapshot, sha1) == 0)
154 return snapshot;
155
155 return NULL; 156 return NULL;
156 filename += strlen(reponame);
157 while (filename && (*filename == '-' || *filename == '_'))
158 filename++;
159 dwim_filename = xstrdup(filename);
160 dwim_filename[strlen(filename) - strlen(fmt->suffix)] = '\0';
161 for_each_tag_ref(ref_cb, NULL);
162 return dwim_refname;
163} 157}
164 158
165void cgit_print_snapshot(const char *head, const char *hex, const char *prefix, 159void cgit_print_snapshot(const char *head, const char *hex,
166 const char *filename, int snapshots, int dwim) 160 const char *filename, int snapshots, int dwim)
167{ 161{
168 const struct cgit_snapshot_format* f; 162 const struct cgit_snapshot_format* f;
163 char *prefix = NULL;
169 164
170 f = get_format(filename); 165 f = get_format(filename);
171 if (!f) { 166 if (!f) {
172 ctx.page.mimetype = "text/html"; 167 ctx.page.mimetype = "text/html";
173 cgit_print_http_headers(&ctx); 168 cgit_print_http_headers(&ctx);
174 cgit_print_docstart(&ctx); 169 cgit_print_docstart(&ctx);
175 cgit_print_pageheader(&ctx); 170 cgit_print_pageheader(&ctx);
176 cgit_print_error(fmt("Unsupported snapshot format: %s", filename)); 171 cgit_print_error(fmt("Unsupported snapshot format: %s", filename));
177 cgit_print_docend(); 172 cgit_print_docend();
178 return; 173 return;
179 } 174 }
180 175
181 if (!hex && dwim) 176 if (!hex && dwim) {
182 hex = get_ref_from_filename(ctx.repo->url, filename, f); 177 hex = get_ref_from_filename(ctx.repo->url, filename, f);
178 if (hex == NULL) {
179 html_status(404, "Not found", 0);
180 return;
181 }
182 prefix = xstrdup(filename);
183 prefix[strlen(filename) - strlen(f->suffix)] = '\0';
184 }
183 185
184 if (!hex) 186 if (!hex)
185 hex = head; 187 hex = head;
186 188
189 if (!prefix)
190 prefix = xstrdup(cgit_repobasename(ctx.repo->url));
191
187 make_snapshot(f, hex, prefix, filename); 192 make_snapshot(f, hex, prefix, filename);
193 free(prefix);
188} 194}
diff --git a/ui-snapshot.h b/ui-snapshot.h
index 3540303..b6ede52 100644
--- a/ui-snapshot.h
+++ b/ui-snapshot.h
@@ -1,8 +1,7 @@
1#ifndef UI_SNAPSHOT_H 1#ifndef UI_SNAPSHOT_H
2#define UI_SNAPSHOT_H 2#define UI_SNAPSHOT_H
3 3
4extern void cgit_print_snapshot(const char *head, const char *hex, 4extern void cgit_print_snapshot(const char *head, const char *hex,
5 const char *prefix, const char *filename, 5 const char *filename, int snapshot, int dwim);
6 int snapshot, int dwim);
7 6
8#endif /* UI_SNAPSHOT_H */ 7#endif /* UI_SNAPSHOT_H */
diff --git a/ui-tag.c b/ui-tag.c
index 3aea87d..0e056e0 100644
--- a/ui-tag.c
+++ b/ui-tag.c
@@ -44,36 +44,46 @@ void cgit_print_tag(char *revname)
44 obj = parse_object(sha1); 44 obj = parse_object(sha1);
45 if (!obj) { 45 if (!obj) {
46 cgit_print_error(fmt("Bad object id: %s", sha1_to_hex(sha1))); 46 cgit_print_error(fmt("Bad object id: %s", sha1_to_hex(sha1)));
47 return; 47 return;
48 } 48 }
49 if (obj->type == OBJ_TAG) { 49 if (obj->type == OBJ_TAG) {
50 tag = lookup_tag(sha1); 50 tag = lookup_tag(sha1);
51 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) { 51 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) {
52 cgit_print_error(fmt("Bad tag object: %s", revname)); 52 cgit_print_error(fmt("Bad tag object: %s", revname));
53 return; 53 return;
54 } 54 }
55 html("<table class='commit-info'>\n"); 55 html("<table class='commit-info'>\n");
56 htmlf("<tr><td>Tag name</td><td>%s (%s)</td></tr>\n", 56 htmlf("<tr><td>Tag name</td><td>");
57 revname, sha1_to_hex(sha1)); 57 html_txt(revname);
58 htmlf(" (%s)</td></tr>\n", sha1_to_hex(sha1));
58 if (info->tagger_date > 0) { 59 if (info->tagger_date > 0) {
59 html("<tr><td>Tag date</td><td>"); 60 html("<tr><td>Tag date</td><td>");
60 cgit_print_date(info->tagger_date, FMT_LONGDATE, ctx.cfg.local_time); 61 cgit_print_date(info->tagger_date, FMT_LONGDATE, ctx.cfg.local_time);
61 html("</td></tr>\n"); 62 html("</td></tr>\n");
62 } 63 }
63 if (info->tagger) { 64 if (info->tagger) {
64 html("<tr><td>Tagged by</td><td>"); 65 html("<tr><td>Tagged by</td><td>");
65 html_txt(info->tagger); 66 html_txt(info->tagger);
66 if (info->tagger_email) { 67 if (info->tagger_email) {
67 html(" "); 68 html(" ");
68 html_txt(info->tagger_email); 69 html_txt(info->tagger_email);
69 } 70 }
70 html("</td></tr>\n"); 71 html("</td></tr>\n");
71 } 72 }
72 html("<tr><td>Tagged object</td><td>"); 73 html("<tr><td>Tagged object</td><td>");
73 cgit_object_link(tag->tagged); 74 cgit_object_link(tag->tagged);
74 html("</td></tr>\n"); 75 html("</td></tr>\n");
75 html("</table>\n"); 76 html("</table>\n");
76 print_tag_content(info->msg); 77 print_tag_content(info->msg);
78 } else {
79 html("<table class='commit-info'>\n");
80 htmlf("<tr><td>Tag name</td><td>");
81 html_txt(revname);
82 html("</td></tr>\n");
83 html("<tr><td>Tagged object</td><td>");
84 cgit_object_link(obj);
85 html("</td></tr>\n");
86 html("</table>\n");
77 } 87 }
78 return; 88 return;
79} 89}
diff --git a/ui-tree.c b/ui-tree.c
index e27e796..4b8e7a0 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -45,26 +45,28 @@ static void print_object(const unsigned char *sha1, char *path)
45 start = 0; 45 start = 0;
46 lineno = 0; 46 lineno = 0;
47 while(idx < size) { 47 while(idx < size) {
48 if (buf[idx] == '\n') { 48 if (buf[idx] == '\n') {
49 buf[idx] = '\0'; 49 buf[idx] = '\0';
50 htmlf(linefmt, ++lineno); 50 htmlf(linefmt, ++lineno);
51 html_txt(buf + start); 51 html_txt(buf + start);
52 html("</td></tr>\n"); 52 html("</td></tr>\n");
53 start = idx + 1; 53 start = idx + 1;
54 } 54 }
55 idx++; 55 idx++;
56 } 56 }
57 if (start < idx) {
57 htmlf(linefmt, ++lineno); 58 htmlf(linefmt, ++lineno);
58 html_txt(buf + start); 59 html_txt(buf + start);
60 }
59 html("</td></tr>\n"); 61 html("</td></tr>\n");
60 html("</table>\n"); 62 html("</table>\n");
61} 63}
62 64
63 65
64static int ls_item(const unsigned char *sha1, const char *base, int baselen, 66static int ls_item(const unsigned char *sha1, const char *base, int baselen,
65 const char *pathname, unsigned int mode, int stage, 67 const char *pathname, unsigned int mode, int stage,
66 void *cbdata) 68 void *cbdata)
67{ 69{
68 char *name; 70 char *name;
69 char *fullpath; 71 char *fullpath;
70 enum object_type type; 72 enum object_type type;
@@ -97,25 +99,25 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
97 html("</a>"); 99 html("</a>");
98 } else if (S_ISDIR(mode)) { 100 } else if (S_ISDIR(mode)) {
99 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, 101 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head,
100 curr_rev, fullpath); 102 curr_rev, fullpath);
101 } else { 103 } else {
102 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head, 104 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head,
103 curr_rev, fullpath); 105 curr_rev, fullpath);
104 } 106 }
105 htmlf("</td><td class='ls-size'>%li</td>", size); 107 htmlf("</td><td class='ls-size'>%li</td>", size);
106 108
107 html("<td>"); 109 html("<td>");
108 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev, 110 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev,
109 fullpath, 0, NULL, NULL); 111 fullpath, 0, NULL, NULL, ctx.qry.showmsg);
110 if (ctx.repo->max_stats) 112 if (ctx.repo->max_stats)
111 cgit_stats_link("stats", NULL, "button", ctx.qry.head, 113 cgit_stats_link("stats", NULL, "button", ctx.qry.head,
112 fullpath); 114 fullpath);
113 html("</td></tr>\n"); 115 html("</td></tr>\n");
114 free(name); 116 free(name);
115 return 0; 117 return 0;
116} 118}
117 119
118static void ls_head() 120static void ls_head()
119{ 121{
120 html("<table summary='tree listing' class='list'>\n"); 122 html("<table summary='tree listing' class='list'>\n");
121 html("<tr class='nohover'>"); 123 html("<tr class='nohover'>");